This is a submission for the Hacktoberfest Open-Source AI Challenge: Week 1: Touch Grass
What I Built
Weekend Micro-Adventure is a one-screen web app that gets you off the screen.
You pick three things β how long you have, how you'll get there, and what mood you're in β type a city, and press Send me out. You get exactly one card: one real place nearby, a short plan, and a leave-by time. There is one button underneath, Not feeling it, which gives you a different place exactly once. Then it's gone.
There is no feed, no list of options, no login, no settings. The design constraint I set myself was that the screen must be the shortest part of the experience, so I kept a timer in the footer that shows how long you've been staring at it. The intended caption for the demo is "Screen time: 10s. Outside time: 45+ min."
It's live: weekend-micro-adventure.onrender.com
The reason a language model is genuinely useful here isn't "it writes text." It's three specific jobs:
- Pick the best place from a shortlist for your mood and constraints.
- Turn raw search results into something readable in under ten seconds.
- Be honest. If a detail isn't in the data, say "check before you go" instead of making it up.
That third one was the hard part, and most of this write-up is about how I made it hold.
Demo
Live app deployed on Render:
weekend-micro-adventure.onrender.com
One screen, three choices, one card.
Type the city, address or share your location, choose how much time you can spend on your adventure, how you want to get there and the vibe you want.

Click on "Send me out" and Gemma will prepare a plan for your adventure, taking into account the current weather, time and the which places are open. You can always re-roll for a different plan using "not feeling it" button.

Code
dumnicki
/
weekend-micro-adventure
Weekend Micro-Adventure - Discover new places around you and plan your adventure
Weekend Micro-Adventure
One screen. Pick time, transport, vibe, location. Press Send me out. Get one real adventure card β one place, a short plan, and a leave-by time. No lists, no feed, no login.
Gemma (open-weights) runs through Backboard. Places come from SerpApi Weather and daylight from Open-Meteo (free, no key).
Run locally
pip install -r requirements.txt
cp .env.example .env # then fill in the keys
uvicorn app.main:app --reload
Open http://127.0.0.1:8000 . Health check: GET /api/health -> {"status":"ok"}.
Run tests
pytest -q
Tests never call a live API β they use saved fixtures and mocks.
Current model
BACKBOARD_MODEL=google/gemma-4-26b-a4b-it
BACKBOARD_PROVIDER=openrouter
scripts/list_models.py lists every Backboard model matching gemma (there are ~1000,
mostly community fine-tunes on the featherless provider). Pick the openrouter entry
for the Google-namespaced model: 262k context, versus 32k on the featherless duplicate.
Typical model latency
A one-shot generate_json call against Gemma returns in about 1.5β2s. Thatβ¦
How I Built It
Stack: Gemma 4 26B-A4B (open weights) served through Backboard, SerpApi for real places with coordinates and ratings, Open-Meteo for weather and sunrise/sunset (free, no API key), FastAPI, and one index.html with plain JavaScript. No front-end framework, no build step, no database.
The one architectural decision that mattered
Code decides, the model writes.
Every decision that can be made from a number is made in Python: distance filtering, the time math, the daylight cutoff, scoring, deduplication, even the radius per transport. The model does the only two things code is bad at β picking one item from eight, and writing it in a human voice.
This is not a stylistic preference. It meant that when the model failed β and it did, repeatedly β the failure degraded to a plain template card instead of an error page. The app has never once returned a 500. Model timeout, malformed JSON, Backboard down: all of it falls back to a code-built card.
It also made the model swappable. BACKBOARD_MODEL is an environment variable. Gemma does the least critical job in the pipeline, so swapping it out doesn't break the safety properties β it just changes the prose.
Controlling hallucination
The risk is a made-up trail or a made-up opening time, and a confident lie about where to walk is worse than no answer. Five layers:
- The model only ever sees 8 pre-filtered candidates and must pick by ID.
- The system prompt forbids anything not present in the candidate data.
- The server validates the output β this is the part I got wrong twice.
- Every card carries a
check_before_you_gofield. - The place name, address, coordinates and map link come from code, never from the model. A hallucinated title cannot move you to the wrong park, because the model isn't allowed to supply the coordinates.
This is the same territory people are actively writing about β Stop prompting for valid JSON: an LLM output layer that does not break and LLM Structured Outputs and JSON Schema: Tool Calling That Never Drifts β and my experience matched theirs: schema validation is table stakes, but schema validation does not catch a wrong number inside a valid shape.
The bug that taught me the most
My first prompt run scored 7 out of 10. Both failures were the same mistake, and it was a genuinely interesting one:
The model kept setting
duration_minutesto the total time budget, ignoring the shorteractivity_minutes_availableon the specific place it had chosen.
It read the global budget (60 minutes) instead of the per-candidate allowance (30 minutes). A perfectly well-formed JSON object with a confidently wrong number in it β which no schema catches.
The obvious fix doesn't work, because the rule was correct in the first draft. I had already written "duration_minutes must not exceed activity_minutes_available", and the model ignored it. Burying a hard constraint in a list of eight soft rules doesn't make it a hard constraint.
What worked was making it structural instead of advisory:
- A
max_duration_minutescopy-target field sitting next to each candidate. - A worked example in the system prompt showing the wrong answer alongside the right one, including the exact wrong numbers.
- Restating the rule in the user template immediately below the candidate JSON, ending with the instruction: "Do not use {budget_minutes}."
Next run: 10 out of 10. The lesson generalizes past this app β if a small open-weight model won't follow a constraint stated in prose, stop stating it in prose and change the shape of the data it's reading.
The bug my tests could never have caught
Later, a live smoke test returned a card where check_before_you_go was the literal string "None".
That passed every single validation rule I'd written. It's a non-empty string in a string field. My entire suite β 244 tests β was green while this bug existed, because every test used a mocked model that returns well-formed text.
Tests verify structure. Only a real call shows you what the model actually emits. I now treat bare null markers (None, N/A, null) as empty fields, which triggers the retry path. And I added used_fallback to the API response specifically so I can assert that Gemma wrote the card rather than the template β the smoke test now fails the deploy if used_fallback is ever true.
That field exists for one reason: the whole claim of this project is that a model did the work, and I wanted that claim to be checkable rather than assumed.
Smaller things worth knowing
- A per-request cache was spending money on every re-roll. The re-roll button existed specifically to avoid re-querying SerpApi, but the cache was constructed inside the request handler, so it was always empty. One line moved it to process scope.
- Daylight is planned against the destination's clock, not the server's, using the forecast's UTC offset. Getting this wrong silently schedules the trip against the wrong sunset.
-
SerpApi's docs were wrong twice. They documented a
type_idsfield that doesn't exist in the real response, and omitteddescriptionfrom their sample when it actually appears on ~20% of results. Both only showed up by checking a saved real response. - 244 tests, none of which touch the network.
Why Does Open Innovation Matter?
The open-weight model is doing the one job where a closed API would have been most tempting, and it turned out to be the least critical job in the pipeline.
Gemma 4 26B-A4B is a mixture-of-experts model β 26B total parameters but only ~4B active β which is why a full request lands in about two seconds and costs almost nothing. That's not a benchmark win; it's what made a 250-search/month free tier and a free hosting tier enough to actually ship and demo this.
More importantly, the open part is the part I was able to inspect and constrain. I could read the model's behavior across ten identical runs, find the precise number it kept getting wrong, and fix it by reshaping the input. With a closed API, the same bug would have been "tune the prompt and hope." Because the weights are swappable and the model is one module behind an interface (app/services/llm.py is the only file that knows Backboard exists), I can run Gemma today and something else tomorrow without touching the validation, the filtering, or the front end.
And the architecture means openness is load-bearing rather than decorative. Because code computes the distances, the times, the daylight cutoff and the map link, the app's correctness does not depend on the model's good behavior. If the model is down, un-swappable, or replaced with something worse, you still get a correct adventure β just in plainer language.
That's the argument I'd make for open models in general: the value isn't only that the weights are free. It's that being able to look closely at a component, and swap it without permission, is what makes the rest of the system trustworthy.
My Agent Session
The full build transcript, including the failures and two misdiagnoses I made along the way:
Prize Categories
-
Best Use of Gemma β
google/gemma-4-26b-a4b-itdoes the selection and prose for every card. - Best Use of Backboard β model access, provider routing, and the one call per request budget.
-
Best Use of SerpApi β
google_mapsengine for real places, coordinates and ratings; the entire candidate pool. -
Best Use of Render β the free web service this runs on, defined in
render.yaml.
Top comments (0)