This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
At 18:27 on Sunday, a machine at Lidl in Galway printed me a voucher for €0.30: two cans at 15 cents each. It's the first deposit I've claimed in two years of living in Ireland. I don't buy drinks. I carry water in a flask.
The cans weren't mine. I picked them up on a walk that an open-weight model planned for me, along with six other bits of litter that paid nothing at all.
There's a story going round 9GAG about someone in Germany who paid for a PS5 with deposit bottles. I couldn't verify it. My number is easier to check, and I kept the voucher.
TL;DR. Return Route plans a walk through the spots where cans and bottles get left behind, and ends it at a deposit return machine. When you get back, one photo of your haul and Gemma counts it. The refund comes from the deposit table, never from the model. Gemma can run in the cloud, on your laptop, or (untested yet) in your phone's browser. I took it on a real walk in Galway: about an hour outside, 8 pieces of litter picked up, 2 that paid, €0.30. You can keep the money or give it away.
What I Built
Ireland has had a deposit return scheme, Re-turn, since February 2024. A can or a small bottle carries 15c, a big bottle 25c, and you get it back from a machine at the supermarket. So every can in a hedge is a refund nobody claimed.
Return Route turns that into a reason to go outside:
- Pick how long you've got: 30 minutes, an hour, 90 minutes or your own time. You can add a note like "finish at Tesco, stay off the main road".
- Gemma plans a loop through the places where drinks get left behind (picnic tables, pitches, viewpoints, benches near an off-licence) and finishes at a return machine. You get a range for what you might make, never a promise.
- Put the phone away. The route goes to Google Maps, and a short briefing is read aloud: where you're going, what to look for, one safety tip.
- Back home, take one photo. Gemma counts the containers, says which ones the machine will refuse (crushed, damaged, no deposit mark) and prints an itemised voucher. Then you keep the money, or switch on Walk for a cause: the app turns green, the refund goes to a cause you name, and Gemma drafts a post to get others walking.
Screens from the live site: a real plan from Terryland Forest Park on Gemma 26B, then the app's sample haul photo. That photo is generated, with deposit stickers added so the marks are readable.
The screen is on at the start and at the end. In between, it's Google Maps in your pocket and your eyes on the ground. That was the rule I designed around: the app's job is to get you out the door and then get out of the way.
It's for people who walk anyway and would like the walk to do something: dog walkers, students, anyone who joins a local cleanup. I volunteer on weekend cleanups in Terryland Forest Park, which is why the demo starts there.
It covers Ireland's Re-turn and 18 other European deposit schemes, each with its own rates and rules. Return Route is unofficial and not affiliated with Re-turn or any scheme operator.
Demo
The full walk, from planning at home to the voucher, filmed on Ray-Ban Meta glasses and my phone (12 minutes, with chapters):
The one-minute version:
Try it: return-route.onrender.com. It runs on Render's free tier, so the first load after a quiet spell takes about 50 seconds.
Not in Europe? Open return-route.onrender.com/?demo, or tap the demo button on the home screen. The guided demo plans a real walk from Terryland Forest Park with Gemma, plays it on the map, then shows the actual haul from my walk and the voucher it earned. It never touches your own totals.
I took it outside
Sunday 11 October, east Galway, 15°C and overcast. Times are Irish time.
- 17:26: Planned a 90-minute walk at home, with Gemma 4 26B in the cloud. The app's guess: €1.05 to €3.85.
- 17:30: Out the door. No gloves. (Bring gloves.)
- Before I'd left the house: a Monster can with the Re-turn logo, not crushed. 15c.
- On the road: a crushed can (the machine refuses crushed ones, but it's still litter, so it went in the bag), a wrapper, and an off-licence the planner had scored as a hotspot.
- Stop 1, the park: spotless. I did find a playground five minutes from home I'd never seen.
- Between stops: a big bottle, damaged, so it pays nothing. Then a Strong Power can in a hedge: the first real score of the day. A squashed Coca-Cola can and a carton.
- Stop 2, the skate park: busy with kids, so I left it to them. A glass bottle stayed in the bush: glass isn't in Re-turn, and it's not something to handle bare-handed.
- Stop 3: Google Maps led me to a college fence with no way in. It was getting dark and the last stop was far, so I headed for Lidl. My guess on camera: "about 30 cents".
- 18:19: I photographed the haul. The laptop tier failed first, because the laptop was at home. The cloud tier read it: 2 cans, deposit mark not visible, the crushed cans and the bottle struck off. €0.00 for sure, up to €0.30.
- 18:27: The Re-turn machine took 2 cans. Voucher: €0.30. The top of Gemma's range, exactly my guess, and well below the app's estimate.
What I said outside Lidl, unscripted: "the open source project that I just made is working very well … we touched grass, I hope that counts."
What the walk taught me
- My neighbourhood is cleaner than the planner thought. It guessed €1.05 to €3.85 and I made €0.30. I'll take that as good news about east Galway. It's also exactly what the app's hotspot history is for: every logged walk shifts the estimate for the stops you visited.
- The stops were the quietest part. The cans were in hedges, on walls and along the road near the off-licence, not at the picnic tables. The planner's starting scores favour parks, and on a clean Sunday the park was clean.
- A stop behind a fence is a dead end. The map said there was something worth visiting; it didn't say you can't get in.
- Photo close, labels up. From above, in a bag, Gemma couldn't confirm the small Re-turn logos. The + button fixed the count by hand.
- Crushed doesn't pay. Pick them up anyway.
- Bring gloves.
What I changed after the walk
No more stops behind a school gate. Pitches and picnic tables inside school or childcare grounds, and anything mapped as private, are no longer stops, unless the map marks that pitch as open to the public. University campuses stay in, because they're usually open. When I checked the map data for east Galway afterwards, 17 pitches and 6 picnic tables were inside school grounds; the city snapshot had 51 more school pitches and a school playground. They're gone now.
The first version still leaked 6 pitches when the map came from vector tiles. A place near the edge of a tile also appears in the next tile's margin, and that tile only carries a sliver of the school's outline, so the check passed. Each place is now read only from the tile it's actually in.
Code
Return Route
A walk that pays. Gemma plans a loop through the spots where cans and bottles get left behind,
and ends it at a deposit return machine. Keep the refund, or give it away.
Ireland's Re-turn scheme pays 15c for a can or a small bottle and 25c for a big one, so every can in a hedge is a refund nobody claimed. Return Route turns that into a reason to go outside. Tell it how long you've got. It plans a walk past picnic tables, pitches and viewpoints near off-licences, gives you an honest range for what you might make, hands the route to Google Maps, and then gets out of your way.
When you're back, take one photo of your haul. Gemma counts the containers, flags the ones the machine will refuse and works out your refund from the deposit table. Keep the money, or switch to…
MIT licensed, built during the challenge week, with 54 tests. If you only read six files, read these:
| File | What it does |
|---|---|
src/lib/ai/guard.ts |
Every rule Gemma can't break: real stops only, no money talk, no bins |
src/lib/countries.ts |
19 deposit schemes, and the only place money comes from |
src/mastra/workflows/planWalk.ts |
The planning workflow, with map, weather and events in parallel |
src/lib/plan/overpass.ts |
OpenStreetMap places, minus anything behind a school gate |
src/lib/ai/edge.worker.ts |
Gemma 4 E2B in the browser on WebGPU (untested on a phone) |
src/lib/privacy.ts |
What Sentry never sees: coordinates, IPs, cache keys |
Run it on your own laptop:
ollama pull gemma4:e4b
git clone https://github.com/MuhammadMurtuzaHussain/return-route && cd return-route && pnpm install
cp .env.example .env.local # MODEL_PROVIDER=ollama
pnpm dev # http://localhost:3100
How I Built It
Gemma judges, code guarantees
A small model will sometimes invent a park, promise you a fiver, or tell you to go through the bins. So I split the work. Gemma does the parts that need judgement. Plain code owns everything that has to be right, and Gemma can't override it.
| Gemma decides | Code guarantees |
|---|---|
| Which stops, in what order, and why each one | Every stop is a real place from the map, and the walk fits your time |
| What's in the photo: material, size, mark visible, crushed or not | What it pays, from the deposit table |
| What to look for, and one safety tip | The place names in the spoken briefing |
| One opening line for your post | The count, the amount and the cause in that post |
Gemma picks from a list of real places, and a stop it invents is dropped. A route that's too long is trimmed against the real walking path. Money never comes from the model: any sentence where Gemma mentions an amount is removed before it's shown or read aloud.
/** Gemma's words are read aloud; they must not promise money the app did not compute. */
export function stripMoney(s: string): string {
return s
.split(/(?<=[.!?])\s+/)
.filter((sentence) => !/[€£$]\s?\d|\d+\s?(euro|eur|cent|kr|zł|ft)\b/i.test(sentence))
.join(" ")
.trim();
}
/** Bins belong to the people who rely on them. Keep "leave bins alone", drop "look in the bins". */
export function leaveBins(s: string): string {
return s
.split(/(?<=[.!?])\s+/)
.filter((sentence) => !/\bbins?\b|pfandring/i.test(sentence) || /\b(leave|don'?t|do not|never|avoid|not)\b/i.test(sentence))
.join(" ")
.trim();
}
The walk showed why the split matters. Gemma couldn't see the deposit marks in my bag photo, so it didn't guess. It reported "mark not visible", and the code turned that into a range: €0.00 for sure, up to €0.30. The machine paid the top of it. When a model that handles money can't see, I want it to say so, and this one did.
That split is also what should let a 2B model in a phone's browser do the same job as a 26B model in the cloud.
One model family, three places
| Where | Model | Where your haul photo goes |
|---|---|---|
| In the cloud | Gemma 4 26B-A4B through Google AI Studio | To Google |
| On your laptop | Gemma 4 E4B through Ollama | Nowhere: it stays on your laptop |
| In your phone's browser | Gemma 4 E2B on WebGPU (transformers.js), about 3.4 GB once | Nowhere: it's read on the phone |
You pick in Settings. The prompts, the guard and the deposit table are the same for all three.
The phone tier is untested. The code is in the repo and the production build bundles the worker and the WebGPU runtime, but I haven't had an Android phone to run it on, so I can't tell you how long the download takes or how fast it plans. Treat it as the part I'd test first.
Planning needs open map, weather and routing services (OpenFreeMap, Open-Meteo, Valhalla), so your start point goes to those whichever tier you pick. What changes between tiers is where your photo and the model's work go.
The planner: a Mastra workflow
place ──► map ║ weather ║ events ──► assemble ──► pick ──► route
│ │ │ │ │ │ │
deposit OSM Open- SerpApi score Gemma Valhalla walking
scheme POIs Meteo (optional) hotspots picks & route, trimmed
for the orders until it fits
country 2-3 stops
The map, the weather and today's events are fetched in parallel. The score is a hotspot prior for each kind of place, times yesterday's weather (a warm evening leaves cans behind), the day of the week, any match or gig nearby, and your own past hauls. Nobody has a live litter feed, so the estimate starts as an honest guess and gets sharper with each walk you log. That history stays on your device. There are no accounts.
If your country has no deposit scheme, the first step stops the workflow with a plain message instead of planning a walk that can't pay. On the phone tier, the server runs the first half of the same workflow, Gemma picks the stops in a Web Worker, and the server checks and routes the result.
Here's one plan from the live site, as Sentry traced it. The Gemma call is 7.2 of the 8.7 seconds:
Measuring it
I wrote 8 Galway walks (different starts, lengths and notes like "finish at Lidl" or "stay off the main road") and ran them through both server tiers before and after tightening the planner:
| Tier | Plans the guard didn't touch | Too long | Invented stop | Finished where asked | Time per plan |
|---|---|---|---|---|---|
| Cloud, 26B-A4B, before | 7/8 | 1 | 0 | 4/5 | 5.2 s |
| Cloud, 26B-A4B, after | 8/8 | 0 | 0 | 5/5 | 5.3 s |
| Laptop, E4B, before | 6/8 | 2 | 0 | 4/5 | 5.7 s |
| Laptop, E4B, after | 7/8 | 0 | 1 | 5/5 | 5.4 s |
The fix was mostly code, not prompting: for every return point on offer, the planner works out which stops still fit if the walk ends there, a shop you name becomes the finish, and two stops in the same field get distinct names. The one stop E4B invented was caught and dropped.
To be clear about what this measures: it shows Gemma follows the rules. It doesn't show the stops have cans. Only walking tells you that, and on my one walk, the park was clean.
The haul reader has an eval harness too (pnpm eval), but only one real haul so far: the bag from this walk. Both server tiers read it as 0 eligible, because the marks were too small from above. The real answer was 2. That's why the camera screen says "labels up, nothing stacked", and it's the first thing I'll measure properly.
When the cloud gets turned away
Locally, everything worked. On Render, planning failed. Sentry showed why: every Overpass mirror was answering the server with errors, and the place lookup and weather service were returning 429. Render's free tier sends traffic from IP addresses shared by many apps, and public map servers rate-limit them. A laptop never sees that.
So the map now has a chain of fallbacks:
- Render Key Value, which caches each neighbourhood for 12 hours.
- A bundled OpenStreetMap snapshot of Galway, so the demo never waits on a map server.
- OpenFreeMap vector tiles from a CDN. A 30-minute area took 1 to 2 seconds in Galway, Berlin, Copenhagen, Amsterdam and Vienna. Return machines come from Overpass alongside, with a 6-second budget.
- Full Overpass.
- The walker's own browser, which fetches the map from their connection and plans again.
The side effect is that plans now work anywhere in Europe, not just in Galway. The whole service is one render.yaml Blueprint: the web service and the Key Value cache, deployed on every push to main.
What Sentry sees, and what it shouldn't
Every Gemma call is an agent span with the model, tokens and latency, so I can see the planner and the haul reader side by side. Prompts and replies are never sent: the Input tab is empty on purpose.
Every time the guard overrules Gemma, that's an issue tagged with the tier and the model. The one at the top of the guard list is real: E4B planned a route too long for the time. The three below it I triggered on purpose, to check that the phone tier's picks are guarded and reported too.
And then I found my own walk in it. Taking screenshots for this post, I searched the traces and found 415 weather spans from the last two weeks with the walker's position in the URL: latitude=53.2845&longitude=-9.0420, accurate to about 10 metres, from both the browser and the server. The cache spans were worse: the rate limiter's key held the walker's IP address, and the map cache's key held rounded coordinates. I had a privacy block in the Sentry config, and it didn't cover either.
The fix is one small file shared by the browser, server and edge configs. URLs lose their query strings, and cache keys are cut down to their kind:
/** "https://api.open-meteo.com/v1/forecast?latitude=53.28…" → "https://api.open-meteo.com/v1/forecast" */
const URL_QUERY = /(https?:\/\/[^\s?#"]+)\?[^\s#"]*/g;
/** "incr rr:rl:plan:<ip>:29862636" → "incr rr:rl"; "get rr:area:v2:53.28,-9.04,2500" → "get rr:area" */
const CACHE_KEY = /\b(rr:[a-z]+)(?::[^\s"]*)?/g;
After it deployed, I planned a fresh walk on the live site and opened its trace. The weather span now reads GET https://api.open-meteo.com/v1/forecast, the rate limiter's span reads incr rr:rl, and there isn't a coordinate or an IP anywhere in it. The spans sent before the fix will age out with Sentry's retention.
Things that went wrong
-
Gemma sent people into bins. It suggested checking bins for bottles. In Germany, people leave bottles on a Pfandring for those who collect deposits for a living.
leaveBinsdrops any sentence about bins unless it says to leave them alone. - Titles like "Park Waste Collection" and "Eyre Square Park Run". A title now has to name a real place on the route, and "run" becomes "loop".
- "Based on the image, I have identified 7 containers." The narration is stripped before the voucher shows Gemma's notes.
- Cities that map every bin ranked "Bins (4)" as their best stop. Street furniture is now capped, and bins only count inside a park.
- Two stops called "Pitch, Wildflower Meadow". Look-alikes are now "east end" and "west end", and the briefing never says the same park twice.
-
Map markers drifted 30, 60 and 90 pixels down the page. Setting
positionon a custom MapLibre marker overrides its own positioning. Don't. - A school outline cut off at a tile edge let pitches inside school grounds through (above).
- My privacy block had gaps (above).
- Gemma 4 E4B overcounts busy hauls by about one. The voucher shows every item, so you can correct it before it counts.
Why Does Open Innovation Matter?
When the payout is 15 cents a can, the AI has to cost nothing. A walk that earns €0.30 can't pay for API calls. On a laptop, Gemma costs nothing per plan and nothing per photo, so the maths of the whole idea only works with an open model.
Your photo doesn't have to leave your device. On the laptop tier, the haul photo is read by Gemma on your own machine. The phone tier is built to do the same in the browser, though I haven't tested it on a phone yet. Your walk history stays in your browser too: there are no accounts. A walk history is a map of where you live and when you're out. It shouldn't sit on someone else's server, and after this week, it doesn't sit in my Sentry either.
I could build a box around the model. Gemma runs inside my own pipeline, so every output passes through code I wrote and tested before anyone sees it. The same family runs at three sizes in three places, so I wrote the prompts and the guard once, and Sentry tells me which size breaks which rule.
It's open all the way down. Map data from OpenStreetMap, tiles from OpenFreeMap, walking routes from Valhalla, weather from Open-Meteo, the workflow on Mastra, the phone model on transformers.js. None of them needs a key. The only part that does is the optional cloud tier.
Where a closed model might do better: reading small logos on crumpled cans in a bag. I haven't tested one, so I can't say it would. Both Gemma sizes I tried missed the marks on my real haul, and the honest fix so far is a better photo, not a bigger model.
What it can't do yet
- The hotspot scores are educated guesses. They're based on what tends to attract drinkers (benches, pitches, off-licences), and my one real walk says roadsides beat parks.
- Reading deposit marks from a bag photo is unreliable. Close up, labels up, it works. From above, in a bag, it doesn't.
- The phone tier is untested on a real phone.
- Europe only: 19 schemes. The UK's starts in October 2027. US states with bottle bills aren't in yet.
- Free hosting: the first plan after a quiet spell waits about 50 seconds for Render to wake up.
My Agent Session
I built Return Route with Claude Code over three days, across five sessions: from reading this challenge, through the brain dump that became the PRD, the redesign, the Render outage, the videos, to the fixes in this post. This is a curated version: my prompts trimmed, Claude's turns summarised, and the tool calls real.
Prize Categories
- Best Use of Gemma: Gemma 4 is the only model, in three places (26B-A4B through AI Studio, E4B through Ollama, and E2B in the browser on WebGPU, which is built but untested on a phone), with one guard around all of them. Gemma plans the walk, reads the haul photo and drafts the post; code owns the stops and the money. Measured over 8 walks per server tier, before and after, and tested on a real walk that ended in a real voucher.
-
Best Use of Render: one
render.yamlBlueprint runs the app and a Render Key Value cache shared by the map, events and rate limits, deployed on every push. Render's shared outbound IPs taught me how public map servers treat cloud traffic, and the five-step fallback that came out of it makes plans work anywhere in Europe. -
Best Use of Mastra: planning is a Mastra workflow,
place → map ‖ weather ‖ events → assemble → pick → route, with the map, weather and events steps in parallel. It stops early with a plain message when a country has no scheme, and the phone tier runs its first half on the server and the Gemma step on the phone. -
Best Use of Sentry Agent Tracing: each plan is a trace with a span per workflow step and
gen_aiagent spans for the planner and the haul reader, with model, tokens and latency, and no prompts. Every guard override is an issue tagged with tier and model. Sentry found the Render outage that local runs never showed, and, while I was writing this post, a privacy hole in my own setup, which is now fixed.
Built solo.
If there's a deposit scheme where you live, take it out for half an hour and tell me in the comments what you found. And if you only find clean parks, that's useful too: it's what the planner needs to learn.
Built with AI assistance (Claude Code). I reviewed, tested and edited everything. The walk, the voucher and the evaluation numbers are real.








Top comments (2)
tr.ee/dev-to
no