DEV Community

Cover image for Hacktoberfest Challenge Week 1: Touch Grass - Nature Quest
Marco Ramírez
Marco Ramírez

Posted on AI-assisted

Hacktoberfest Challenge Week 1: Touch Grass - Nature Quest

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

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

What I Built

Nature Quest is an offline scavenger hunt for families and kids. You pick a place (park, forest, garden, urban walk), how many things to find (5, 8 or 12) and the age of the youngest player. Then an open-source AI model running on the phone writes a hunt list for that place, season and age: "a red leaf", "something with a spiral shape", "bark with moss on it". The app reads the list aloud and asks everyone to put the phone away.

When someone finds something, they tap it on the list and take one photo. The same model checks it, answers with a short, encouraging message (plus a friendly hint if it is not a match), and reads that aloud too. When the hunt ends, there is a bronze, silver or gold medal and the time spent outside.

The theme was "Touch Grass", so every design decision started from one question: how do I make the screen the shortest part of the experience?

  • Setup is one screen with everything pre-selected, so starting a hunt is one tap.
  • The list and every piece of feedback are read aloud, so nobody needs to look at the phone.
  • Big tap targets and a high-contrast palette, because the phone is used outdoors in the sun, for a few seconds at a time.
  • One photo per find. No camera roll, no feed, nothing to scroll.

It is for families and kids on a walk, with an adult, in English and Spanish. The first screen always says: hunt with an adult, only look and take photos, never pick or touch anything.

Setup The hunt, written on the phone Medal and time outside
Nature Quest setup screen with place, number of items and age pre-selected A hunt list with eight items such as a patch of yellow leaves and a textured gray stone Summary screen with a bronze medal and the items found

Demo

There is no web demo, because the whole point is that it runs on a phone.

  • Install it: Nature Quest 1.0.1 on GitHub Releases (signed APK, about 55 MB, Android 12+, 64-bit ARM). On first launch the app offers to download the AI model (2.59 GB, Wi-Fi recommended). That is the only time it uses the internet.

Taking it outside

WIP

Code

GitHub logo RZEROSTERN / hacktoberfest-week2

Second submission for hacktoberfest. The theme: Touch Grass

Nature Quest

An offline nature scavenger hunt for families and kids. The AI runs on the phone; the fun happens outside.

Nature Quest is an Android app. You pick a place (park, forest, garden, urban walk), how many things to find, and the age of the youngest player. An open-source AI model running on the phone writes a hunt list ("a red leaf" "something with a spiral shape", "bark with moss on it"), reads it aloud, and invites everyone to put the phone away When someone finds something they take one photo; the same on-device model checks it, answers with a short, encouraging message (also read aloud) and gives a friendly hint if it is not a match. At the end there is a bronze, silver or gold medal and the time spent outside.

The phone is a tool for a few seconds at a time. The screen is…

MIT licensed (plus a Beerware clause). The repo has 154 unit tests, the architecture in the README, and docs/DECISIONS.md, a log of 32 technical decisions with the reasons behind each, written as I went. docs/BENCHMARKS.md has every number quoted below.

How I Built It

The open pieces. The model is Gemma 4 E2B (open weights, Apache-2.0) in LiteRT-LM's .litertlm format. It runs through LiteRT-LM (Apache-2.0), Google's on-device runtime, with no network and no server. The rest is a normal Android stack: Kotlin, Jetpack Compose, Hilt, CameraX and Android's own text-to-speech. Nothing closed runs in the app. (I built it with Claude Code, which is not open source. It was my pair programmer, not part of the product.)

The shape of it. The app talks to the model through one interface, InferenceEngine, so ViewModels and use cases never touch LiteRT-LM and tests use a fake. The model answers in strict JSON, which I parse with kotlinx.serialization. If the JSON is bad I retry once, then fall back to something safe: "I'm not sure, try another photo", or a built-in hunt list. Prompts are versioned text files, not strings in Kotlin.

I benchmarked before I built. The first real step was a debug-only screen that loaded the model and timed it on my Pixel 10. That changed the design:

  • The CPU backend was about twice as fast as the GPU: a hunt list took 5.7 s on average on CPU and 11.9 s on GPU, and the first GPU load took 25 s against 3 s. Google's published numbers for a flagship phone did not carry over to mine.
  • Decoding is slow (7 to 23 tokens per second), so every answer is kept short. "Twelve words at most" in a prompt is a performance feature.
  • The phone slows about 1.6x after 25 minutes of back-to-back runs, even though the OS reported no thermal throttling. So I picked settings that hit my targets in the slow state: 140 visual tokens and a 640 px photo. A photo check takes 4.5 to 5 s on a cool phone and 5.7 to 6.5 s on a warm one.
  • Peak memory is 2.2 to 2.6 GB, so the app loads the model for each use and releases it right after. A warm reload is about half a second, and memory drops to about 240 MB.

Safety cannot live in the prompt. The prompt asks for safe items. The model still produced "a coiled snake", "orange fungus" and "a bird's nest hidden in a hollow", and later "a cloud shaped like a deer". For a kids' app that is not acceptable, so every item now passes a deterministic validator in code (English and Spanish word lists: no picking or touching, no animals, insects or mushrooms, nothing near water or roads, no climbing, nothing to eat, no photos of people). Rejected items are regenerated, and if the model still falls short, a built-in safe list fills the gap. The model's feedback is checked too, because it is read aloud to children.

Bugs the phone found that tests did not.

  • The same settings produced the identical hunt three times in a row. LiteRT-LM's sampler seed defaults to 0, which makes sampling deterministic. Each request now gets a random seed.
  • A regex that passed every JVM test crashed on Android, because Android's regex engine rejects a bare } that Java accepts.
  • Cancelling the Kotlin Flow does not stop native inference. You have to call cancelProcess() yourself.
  • CameraX's in-memory JPEG already carries its EXIF rotation, so rotating it again turns the photo sideways.
  • A Compose cleanup effect unbound the camera I had just bound. It read state when disposed instead of when created.

Privacy and delivery. Photos are processed in memory. A small copy of each found photo stays in private cache only for the summary and is deleted when you close it, unless you tap "Save to gallery". The only code that opens a network connection is the model downloader: it resumes interrupted downloads, pins an exact model revision, and verifies the SHA-256 before using the file. I tested that on the phone by killing the app at 17% and resuming. The manifest asks for CAMERA, INTERNET (download only) and network state.

Limits, stated plainly. I could not measure how accurate the photo check is: I did not have a set of real photos to score the model against, so treat its verdicts as fun and encouraging, not as a judge. I tested on one phone. Reading aloud needs a voice installed for the phone's language.

Why Does Open Innovation Matter?

Four things an open model made possible that a closed API would not:

  • It works with no signal on the trail. After a one-time download, there is no server to reach. The people this is for are standing in a field.
  • Kids' photos never leave the phone. With a hosted model, "check this photo" means sending a child's picture to someone else's server. Here it is a function call on the device, and the only network code in the app is the downloader. Because the code is open, anyone can check that claim.
  • It costs nothing per hunt. No API bill, so a family, a school or a scout group can use it as much as they like. The cost is the phone's time: about 5 to 12 seconds per hunt list and 5 to 7 per photo.
  • It is swappable and pinned. The model is a file plus one entry in ModelSpec (URL, size, SHA-256), behind an interface. The app behaves identically next year, because the exact revision is pinned and checksummed instead of changing under me. And I could turn the knobs a hosted API hides: backend (the CPU-versus-GPU finding above), image resolution and memory.

The honest counterpoint: a server GPU would be faster, and probably more accurate. I traded speed and a 2.6 GB first download for privacy and offline use. For a hunt in a forest, I think that is the right trade, but it is a trade.

My Agent Session

I built Nature Quest with Claude Code, from an empty repository to two signed releases. Here is the whole conversation: my prompts, Claude's replies and every tool call, from the benchmark spike to the release. Claude's private reasoning is not included, long tool outputs are truncated, and anything personal (emails, local paths, device serial numbers) is scrubbed.

Building Nature Quest: an offline scavenger hunt with Gemma 4 on-device (LiteRT-LM), GitFlow end to end
You

let's start the next development:

<pasted_content id="5de6">

Context

I'm entering the DEV Hacktoberfest Open-Source AI Challenge, Week 1: "Touch Grass".
The project MUST comply with these rules:
- Build something with open-source AI at its core that gets people off the screen and
into the world. The best builds make the screen the shortest part of the experience.
- Open-source AI must be what makes the product work: at runtime, NO closed-model APIs
and NO server inference. Everything runs on the phone. You're helping me build it, but
the product runs entirely on open components.
- The repo is created and completed within the challenge window, which closes Monday,
October 12, 00:59 Mexico City time.
- Hard internal deadline: the app must be installed and working on my phone by Friday
night. On Saturday I take it outside with real users (that's a bonus criterion), and
Sunday is for the write-up.

All architecture decisions below are already made. Build the whole project end to end,
following the build order, without waiting for confirmation between steps unless a step
explicitly says to stop. Time is the main constraint: always choose the simplest solution
that works.

Git workflow: GitFlow with non-stacked PRs (ESSENTIAL, never skip)

STRICTLY FORBIDDEN: a branch named "main". The production branch is "master".
Never create, push, reference, or target "main" in any command, PR, config, or doc.

Branches:
- master: production only. Changes arrive only through release/* or hotfix/* PRs.
- develop: integration branch. Changes arrive only through feature/* PRs.
- feature/<short-name>: one per build step, always created from the latest develop.
- release/<version>: created from develop when the MVP is ready; PR into master, then
merged back into develop.
- hotfix/<short-name>: from master, only for urgent fixes; PR into master and merged back
into develop.

Rules:
- Never commit directly to master or develop.
- One feature branch per build step, one PR per feature branch, base branch = develop.
- NO stacked PRs: never create a branch from another feature branch, and never open a PR
whose base is anything other than develop (or master for release/hotfix).
- Sequence for every step:
1. git checkout develop && git pull
2. git checkout -b feature/<name>
3. Work with small Conventional Commits.
4. Run lint + unit tests for the parts touched; all must pass, and the app must build.
5. Push and open the PR with gh pr create --base develop.
6. Merge it with gh pr merge (merge commit, delete the branch).
7. Only then start the next step from the updated develop.
- PR description: what changed, why, how it was tested, and whether DECISIONS.md was
updated. PR title follows Conventional Commits.

The product: "Nature Quest" (working title)

An offline nature scavenger hunt for families and kids. The phone is only a tool for a
few seconds at a time; the hunt happens outside.

Flow:
1. Setup (one screen, under 30 seconds): pick the place type (park, forest, garden,
urban walk), the hunt length (5, 8, or 12 items), and the youngest player's age range.
The current month and the device language are used automatically.
2. Gemma 4, on device, generates a hunt list suited to the place, the season (central
Mexico in October: end of the rainy season, early fall), and the age range. Items are
observable and safe, e.g., "a red leaf", "something with a spiral shape",
"a feather", "bark with moss on it", "a flower with five petals".
3. The app reads the list aloud with Android's TextToSpeech and invites everyone to put
the phone away. The list stays available with a single tap.
4. When players find something, they open the camera, take one photo, and Gemma 4
verifies whether it matches an item on the list. The response is a short, fun,
encouraging message, also read aloud, and the item is checked off. If the photo
doesn't match, a friendly hint is given.
5. When the hunt ends (all items found or the players end it), a summary screen shows the
items found, the time spent outside, and a medal (bronze, silver, gold).

Safety rules for generated content (non-negotiable):
- Photograph, don't pick: never ask players to pick plants or flowers, collect, touch, or
approach animals, insects, or mushrooms.
- No items that require leaving paths, climbing, going near water or roads, or touching
anything that could be harmful.
- Never suggest eating or tasting anything.
- Encourage kids to hunt with an adult.

Language: all UI strings in Android resources, English as the default (values/) and
Spanish (values-es/). Model prompts and responses follow the device language.

Privacy:
- Photos are processed in memory for verification and never uploaded anywhere.
- During a hunt, photos may be kept only in app-private cache for the summary screen,
and are deleted when the hunt ends unless the user taps "Save to gallery".
- No analytics, no telemetry, no network calls except the one-time model download.

Architecture

Single Android app module, Kotlin, Jetpack Compose, MVVM, Clean Architecture
(data / domain / ui packages), Hilt, Coroutines + Flow, CameraX, Android TextToSpeech,
Gradle version catalog. Room only if the history step is reached.

On-device inference:
- Runtime: LiteRT-LM via its Kotlin Android library (check the official docs at
ai.google.dev/edge/litert-lm and the google-ai-edge/LiteRT-LM repo for the current
artifact, version, and API; never guess).
- Model: Gemma 4 E2B in .litertlm format from litert-community/gemma-4-E2B-it-litert-lm
on Hugging Face. E4B only if the spike shows my phone handles it well.
- Wrap the engine behind a domain interface (e.g., InferenceEngine) so ViewModels never
depend on LiteRT-LM directly and unit tests can use a fake.
- Generation and verification responses use strict JSON, parsed with kotlinx.serialization.
On invalid output, retry once, then fall back to a safe default (for verification:
"I'm not sure, try another photo").
- Model delivery: during development, load the model from a file pushed with adb.
For release, download it once on first launch (Wi-Fi recommended, progress shown,
resumable, checksum verified), stored in app-private storage.

Repo layout:
/app Android app.
/samples Test photos (leaves, flowers, bark, feathers, etc.), already provided.
/docs DECISIONS.md and BENCHMARKS.md.
Root: README.md, CLAUDE.md, .claude/rules/, .gitignore, gradle files.

Build order (each step = one feature branch + one PR into develop)

  1. Git setup: verify the GitHub remote and that gh is authenticated, and confirm that
    master exists and is the default branch on GitHub. If any of these fails, STOP and
    tell me. Create develop from master and push it.

  2. feature/scaffold: Compose app with Hilt, version catalog, navigation, theme,
    English + Spanish string resources, lint configured, docs/DECISIONS.md.

  3. feature/claude-memory: Claude Code project memory, following current Claude Code
    practices. Keep every file short: only what you can't infer from the code and what
    differs from standard conventions.
    a. CLAUDE.md at the repo root (committed), under ~60 lines:

    • One-paragraph project overview and the repo layout.
    • Exact commands: build, install on device, push the model with adb, unit tests, lint.
    • The GitFlow and non-stacked PR rules above, condensed.
    • Workflow: lint + tests + build before each commit; Conventional Commits; update docs/DECISIONS.md when a technical decision is made or changed; check official docs instead of guessing LiteRT-LM, CameraX, or Hugging Face APIs.
    • Non-negotiables: never use a branch named "main" (production is "master"); no closed-model APIs or server inference; photos never leave the device; the safety rules for generated content; all UI strings in resources.
    • A "Gotchas" section, updated as we discover them. b. .claude/rules/inference.md with paths frontmatter scoped to the inference/data packages: engine behind a domain interface; strict JSON + retry once + safe fallback; prompts in versioned files under app/src/main/assets/prompts/; model loading and inference always off the main thread; release the engine when not needed. c. .claude/rules/ui.md with paths frontmatter scoped to the ui package: screens designed to be used in a few seconds; large tap targets and high contrast for outdoor sunlight; no strings outside resources; every long operation shows progress and can be cancelled. d. CLAUDE.local.md (in .gitignore) only for machine-specific settings: my device model, the local path of the .litertlm file, and adb details.
  4. feature/model-spike: integrate LiteRT-LM, load Gemma 4 E2B from the adb-pushed file,
    and add a debug-only screen that (a) generates a hunt list and (b) verifies each photo
    in /samples against a given item. Record in docs/BENCHMARKS.md: model load time,
    time to first token, total time for generation and for one verification, peak memory,
    and the accuracy of verification on /samples.
    Targets on my device: model load under 20 s, verification under 10 s, generation under
    20 s, and correct verification on at least 8 of 10 samples.
    If any target is clearly missed, STOP before opening the PR and show me the results
    with options (E4B vs E2B, smaller image resolution, shorter prompts, fewer tokens).
    Otherwise, continue. Also check whether the Hugging Face model repo is gated; if it
    requires a token to download, STOP and propose how to deliver the model without
    embedding any token in the app.

  5. feature/hunt-generation: setup screen and on-device list generation with the safety
    rules enforced in the prompt and validated in code (reject and regenerate any item
    that violates them).

  6. feature/photo-verification: CameraX capture and on-device verification with feedback
    and hints.

  7. feature/game-loop: checklist, TextToSpeech for the list and feedback, end of hunt,
    summary screen with time outside and medal, private photo cache cleanup.

  8. feature/model-download: first-launch download with progress, resume, and checksum.

  9. feature/history (only if time allows): past hunts stored with Room.

  10. feature/readme: README in English: what it is, who it's for, screenshots, how to
    install the APK and get the model, architecture, benchmarks, the branching model, and
    a "Why open source matters here" section (works with no signal on the trail, kids'
    photos never leave the phone, zero cost per hunt, swappable model).

  11. release/1.0.0: from develop, PR into master, merge, tag v1.0.0, merge master back
    into develop, and create a GitHub Release with the APK attached.

Priority if time runs short: spike > generation > verification > game loop >
model download > TextToSpeech > history. For Saturday's outdoor test, an adb-pushed
model is acceptable; the download step matters for judges installing the APK.
Even when cutting scope, never skip the GitFlow sequence.

General rules

  • Code, comments, commits, PRs, and README in English.
  • Don't invent LiteRT-LM, CameraX, TextToSpeech, or Hugging Face APIs or flags: when unsure, check the official docs before writing code, and tell me if something isn't documented.
  • Unit tests for ViewModels, JSON parsing, and safety validation using a fake InferenceEngine.
  • Ask before installing anything globally or touching anything outside the repo.
  • Keep docs/DECISIONS.md logging each technical decision and why; I'll use it for the post.
  • Whenever I correct you on something that should apply to future sessions, propose adding it to CLAUDE.md or the right rules file instead of only fixing the code. </pasted_content id="5de6">
Agent

Starting with Step 0: verifying the git remote, gh auth, and that master is the GitHub default branch.

Agent

Local repo has a remote and no commits yet. Now checking what GitHub has.

Agent

GitHub's side is empty, so the master check fails. Before stopping, I'll run a few read-only checks on the toolchain so I can report everything in one go.

Agent

Stopped at Step 0. The GitHub repo exists and gh is authenticated, but master doesn't exist on GitHub yet, so the check you asked me to stop on fails.

Check Result
Remote ✅ origin is [REDACTED]:RZEROSTERN/hacktoberfest-week2.git, public, created 2026-10-05
gh auth ✅ logged in as RZEROSTERN with repo scope
master is the default branch on GitHub ❌ The repo is empty: no branches, and defaultBranchRef is blank
Local repo ⚠️ Branch master, zero commits, so I can't create develop from it yet

Decision for you: an empty repo gets its default branch from the first push, and develop can't be cut from a branch with no commits. The only way forward is a one-time bootstrap commit on master, which technically breaks "never commit directly to master". I recommend this:

  1. Make a single commit on master containing only .gitignore (with CLAUDE.local.md, local.properties, *.litertlm and build outputs) and a one-line README.md.
  2. Push it. GitHub then sets master as the default, and I confirm that with gh repo view.
  3. Create develop from it, push it, and go straight into feature/scaffold. Every later change goes through the GitFlow sequence.

If you'd rather keep master untouched, you can create it from the GitHub UI by adding a README, but the result is the same.

Other findings (none block Step 0; some will block later steps):
- Phone: a Pixel 10 (<device-serial>) is connected over adb.
- Android SDK: found at ~/Library/Android/sdk, but ANDROID_HOME isn't set. I'll point local.properties at it instead of changing your shell.
- JDK: Java 17 (Zulu) is on PATH.
- Gradle: there is no global gradle, so I need a way to generate the Gradle wrapper for Step 1. I'll ask before installing anything.
- /samples is missing. You said it was already provided, but there's no samples/ directory in the repo. I need the photos before Step 3. Drop them in, or tell me where they are.

Reply "go" to use the bootstrap plan above, or tell me if you want something different.

You

make initial commit with README.md so you can continue

Agent
Agent

master is now the GitHub default branch. Creating develop from it and pushing.

Agent

Gradle 9.5.1 is already cached from your previous projects, so I can generate the wrapper from it without downloading anything. Next I'll check current stable versions in the official sources.

Agent

Those summaries disagree with what's cached on your machine (Gradle 9.6.0 vs 9.5.1 cached), so I'll check the primary sources, the Maven metadata and Gradle's version API, directly.

Agent

The primary sources give me real numbers. Two things left before writing the scaffold: AGP 9.4.1's exact Gradle/SDK requirements, and LiteRT-LM's Android requirements (they decide minSdk).

Agent

Now the Gradle wrapper, generated from the Gradle 9.5.1 already cached in ~/.gradle (no global install).

Agent

JAVA_HOME in your shell points at JDK 11, but SDKMAN has Zulu 17.0.7. I'll use that for builds and record it as a machine-specific gotcha for CLAUDE.local.md in Step 2.

Agent

The wrapper is in place. Now the app module: build config, manifest, theme, navigation, placeholder screens, and EN/ES resources.

Agent

Everything is written. Running the first build with lint and unit tests; it may take a few minutes since it downloads Gradle 9.6.0 and the dependencies.

Agent

Installing platform 37 as agreed, into the SDK directory only.

Agent

Platform 37 is installed. Bumping compileSdk and rebuilding with lint and unit tests.

Agent

The build compiled; lint failed, which is what strict lint is for. Reading the report.

Agent

The scaffold builds and launches on your Pixel without crashing. The screen is locked, so I can't see the UI. Next I'm writing docs/DECISIONS.md, then committing and opening the PR.

Agent

Checking what's about to be committed (build outputs and local.properties must be ignored).

Agent

Clean. I'll add .kotlin/ (Kotlin 2.x session files) to .gitignore as a precaution, then commit in three small Conventional Commits.

Agent

Pushing and opening the PR into develop.

Agent

PR #1 is open. Following the app's PR workflow, I'll check its status with the PR tools before merging.

Agent

Step 1 is merged into develop. The stale origin/feature/scaffold ref is just a leftover local tracking ref, so I'll prune it and confirm.

Agent

The page was large, so I'll pull just the sections I need from the saved copy.

Agent

That was a quoting problem on my side (adb splitting the sh -c string), not a permission result. Retrying with the command quoted for the device shell.

Agent

Now logging the decisions from this step, then verifying line count, frontmatter parsing, and that the local file is ignored.

Agent

PR #2 is bound with base develop and no CI checks to wait on. Merging and syncing.

Agent

Step 2 is merged. Step 3 (model-spike) is the risky one, and it needs inputs I flagged at the start, so I'll check what exists now: the samples/ photos and the model file sizes.

Agent

The download is running in the background. Meanwhile, I'll read the LiteRT-LM repo directly for the exact Kotlin API, since summarized docs can drop signatures and I'm not allowed to guess them.

Agent

The model card confirms CPU/GPU Android support, with vision/audio loaded on demand. Now the exact signatures from source, which I need for the engine wrapper.

Agent

The model finished downloading (2.59 GB) and the SHA-256 matches Hugging Face's published checksum exactly. Now writing the spike code: first the dependency and build wiring.

Agent

Before writing the engine wrapper, I'll confirm the exact sendMessageAsync overload that takes Contents and returns a Flow, so I call a signature that exists.

Agent

Both Message and Contents Flow overloads exist. Writing the domain layer, the data layer, the prompts and the unit tests now.

Agent

Now the unit tests for the parser and template, then a compile check before I write the debug spike on top.

Agent

Adding a samples/README.md so the folder exists in git and documents the naming convention the spike uses for expected labels, then a full build with lint.

Agent

Compilation succeeded; lint has findings. Reading them.

Agent

Building, installing on your Pixel, and pushing the model (2.6 GB over USB, copied into the app's private storage, then the temp copy removed).

Agent

I'll put the launch-and-wait cycle in a small scratch script, since I'll repeat it for several configurations.

Agent

It's still running after 10 minutes, so something is slow. Checking the live log to see where.

Agent

Rich data already, and the run seems to have finished or stalled past my head. Looking at the tail of the log.

Agent

The foreground is our spike activity, so the screenshot is safe to view. Finding the Cancel button.

Agent

I wrote "12 GB class" from memory, and I haven't verified the phone's RAM. Checking before it goes into the doc.

Agent

Verified: 11.3 GiB of RAM (a 12 GB phone) and the SoC reports Tensor G5. Correcting the line to the measured value, then updating DECISIONS.md.

Agent

I inserted the new entries before D-009 instead of after it, which breaks the "newest at the bottom" order. Fixing the ordering.

Agent

Final pre-commit checks: CLAUDE.md length, then the full build, lint and unit tests.

Agent

Lint flagged something in the new code. Reading the findings (unit tests are 14/14 and the release APK correctly contains no spike code).

Agent

Easy fix: use the KTX createBitmap (core-ktx is already a dependency).

Agent

Everything passes. One last on-device run with the final build and defaults, to make sure what I'm about to commit actually works.

Agent

Four logical commits: domain contract, data layer plus build wiring, debug spike, and docs.

Agent

PR #3 is bound, based on develop, and has no CI checks. Merging and syncing.

Agent

First the domain layer: settings, the safety validator (the core of this step), prompt/fallback interfaces and the generation use case.

Agent

Now the use case and its collaborators, plus the small changes to existing classes (injectable parser, a dedicated "model missing" exception).

Agent

Retiring the old prompt version and pointing the spike at v2; then the resources (fallback arrays and all new EN/ES strings).

Agent

Now the hunt screen's ViewModel (assisted injection so it's unit-testable without a SavedStateHandle), the screen, and the navigation wiring.

Agent

Two small cleanups in that test file (an awkward runBlocking inside runTest, and a deprecated Locale constructor), then the remaining tests.

Agent

Two straightforward compile errors: a missing getValue import, and weight needing a ColumnScope receiver.

Agent

Everything compiles and passes. Verifying test counts and running lint.

Agent

I included a stray parameter by mistake; resending the edit correctly.

Agent

The foreground is our app, so the screenshot is safe to view.

Agent

My screen-scraping approach produced nothing, so I need to look at what's actually on screen before drawing conclusions.

Agent

Cancel works on the real screen: RSS dropped from 1.82 GB to 247 MB, the app returned to setup, and there were no crashes. Now the full checks, then the docs for this step.

Agent

A different lint task failed this time (lintAnalyzeDebugUnitTest), so let me read why.

Agent

That count includes .png this time (earlier I only grepped .jpg). I'll confirm those are library resources and not our code or photos.

Agent

Those are nine-patch images from the UI libraries; there's no spike code and no photos in the release APK. Now the docs for this step.

Agent

Committing in four logical groups.

Agent

PR #4 is mergeable, with base develop and no CI checks. Merging and syncing.

Agent

Now the data layer for verification: rotation-aware downscaling (with EXIF detection to avoid double rotation), the private photo cache, localized fallback texts and DI bindings.

Agent

Now the UI layer: HuntViewModel backed by the session (so found items show as checked when you return), the verify ViewModel, the CameraX capture composable and the screens.

Agent

I left a stray placeholder function in that screen file. Removing it and tidying the BoxScope import, then updating the hunt screen so items are tappable and show a check once found.

Agent

Now the strings in both languages. The old hunt_ready_hint is no longer used, so lint would flag it; I'm replacing it.

Agent

Two failures. The icons artifact isn't transitively available, and resource merging failed. Reading the resource error first.

Agent

Build and tests pass. Checking counts and lint.

Agent

The flow reached the camera screen and found the shutter, but no photo was captured: no "Checking" screen and no NQ_PHOTO log. Let me see what the app is showing and what CameraX logged.

Agent

The screen had gone to sleep during my build, so that script started from a dead screen. Running the capture flow and then the path checks back-to-back.

Agent

Now the data layer: the session with a clock, the Android text-to-speech speaker, the localized texts, the gallery saver and DI wiring.

Agent

Now the UI layer: the hunt screen with Finish, read-aloud/stop and a leave confirmation; the summary screen and ViewModel; and speech hooks in the verify flow.

Agent

Now the strings for both languages, including plurals.

Agent

Now the tests. First the new fakes (speaker, texts, gallery, clock), then updating the existing tests for the new constructors and adding the new ones.

Agent

The guard test I just added caught something, which is exactly its purpose. Checking which text tripped the safety check.

Agent

The phone's Pictures folder is back to how it was. Now the docs for this step.

Agent

All checks pass. Committing in logical groups.

Agent

Now the strings, then tests for the ViewModels, then a full build.

Agent

The download test is running in the background (I won't touch the phone or reinstall until it finishes). Meanwhile I'll write this step's docs.

Agent

The test already finished. Reading the full result.

Agent

My sed -i doesn't work on macOS, so the script wasn't changed and that run will fail the same way. Fixing it properly with Python and re-running.

Agent

My "wait for the old run" check matched its own command line, so it would wait forever. Stopping that background task and starting the test directly.

Agent

That notification is from the earlier broken run; the real one is now running. Checking its progress.

Agent

Real-time progress is working at roughly 20 MB/s, and 485 MB is on disk when the app is killed. Now the resume phase, where the checksum will tell us whether resuming was byte-correct.

Agent

Now recording the real-device results in the docs, then the full checks.

Agent

All green. Committing in logical groups.

Agent

Per-app language switching works. Setting the app to English and capturing the screens, which also checks the English path end to end (English hunt generation and speech) on the device.

Agent

The model is back in place. Now processing the screenshots: cropping off the status bar (it shows your notification icons) and the gesture bar, and shrinking them for the repo.

Agent

Both upstream pieces are verified as Apache-2.0 and ungated. Writing the license and the README now.

Agent

The two download-screen screenshots aren't used yet; I'll show them in the install section. I'm also logging the license choice as a decision and keeping the README's entry count accurate.

Agent

A signed app-release.apk was produced. Verifying the signature and manifest facts, and confirming that anyone without my keystore can still build (it must fall back to unsigned rather than fail).

Agent

Only the expected files are staged-able; the keystore and its properties don't appear. Committing explicit paths only (never git add -A), then the PR.

Agent

Quick status: the release PR (#10, base master) is open. Next I'll check it, merge it, and tag v1.0.0.

Agent

The release PR's mergeability isn't computed yet. Checking it directly, since master (one initial README commit) and the release branch both touch README.md, then merging.

Agent

master now has the release merge commit. Tagging it v1.0.0 (annotated) and pushing the tag.

Agent

Now merging master back into develop (through a PR, to respect "never commit directly to develop"). I must not delete master when merging this one.

Agent

develop now contains everything on master (0 commits ahead), and master is intact. Cleaning up the finished release branch, then publishing the GitHub Release with the signed APK attached.

Agent

Publishing the GitHub Release, first re-verifying the APK being attached is byte-for-byte the one I verified and recorded.

Agent

The release is published. Verifying what GitHub actually serves: the asset listing, and a fresh download of the public APK compared against my checksum.

Agent

Nature Quest 1.0.0 is built and published, but the photo check has never been scored for accuracy, and the signed release APK has not been installed on your phone. Everything ran through your GitFlow, with 11 pull requests and no branch named main.

Release: https://github.com/RZEROSTERN/hacktoberfest-week2/releases/tag/v1.0.0. The signed APK (55 MB) is attached. I downloaded it from the public URL and the checksum matches, and it passes apksigner. The tag is v1.0.0 on master, and master is merged back into develop.

What works, checked on your Pixel 10 with the real model:
- Setup, hunt list, tap-an-item, camera, photo verdict, read-aloud, finish, medal summary, and photo cleanup all ran without crashes. Both English and Spanish ran, Spanish more.
- Hunt list takes about 5–12 s, a photo check about 5–7 s.
- The 2.59 GB model download takes about 2 minutes on Wi-Fi. I killed the app mid-download, it resumed, and the SHA-256 check passed.
- 148 unit tests pass and lint is clean.
- The release APK asks for CAMERA, INTERNET (used only for the model download) and network state.

What is not verified:
- Photo accuracy is unmeasured. The samples/ folder you said was already provided doesn't exist, and I flagged it at the start. To get the 8-of-10 result, put 10+ real photos in samples/ named like leaf1.jpg or red_leaf_2.jpg, and I'll run the benchmark. If accuracy is poor, the verification settings would need to change.
- The signed release APK has not been installed on any device. I tested the debug build, which is identical except for signing and a debug-only spike screen. Installing the release over the debug build needs an uninstall first, which also deletes the model. It re-downloads in about 2 minutes.
- Finding every item, and a summary with real photos, were never seen on the phone. I can't point your camera at a leaf; the logic is covered by unit tests.
- The speech audio was not heard. I only confirmed the engine started speaking in the log.
- I tested on one phone only.

Decisions that were mine, easy to reverse:
- I skipped step 8 (history with Room): it's marked optional and last in your priority list.
- MIT + Beerware is the license; I read it as MIT being the binding grant and the beer a non-binding request. Edit LICENSE if you meant something else.
- I added a signing PR before the release, because an unsigned APK can't be installed.
- The app uses the CPU backend by default: on this phone it was about 2× faster than the GPU. Details are in docs/BENCHMARKS.md.

What I changed on your phone:
- Installed the debug build, which still has the downloaded model.
- Granted the camera permission to the debug build.
- Saved and then deleted one test image in Pictures; the folder is back to normal.
- Reset the app's language override.
- To use the release build instead, run adb uninstall mx.dev1.naturequest and then install the APK.

Back up keystore/nature-quest-release.jks and keystore.properties privately. They are git-ignored and exist only on this Mac. If you lose them, installed apps can't be updated in place.

Every technical decision is logged in docs/DECISIONS.md (31 entries), which should cover your write-up.

You

Do we have i18n for it? It's a feedback given from other participants. At least english and spanish

Agent

Yes, English (default) and Spanish both exist. Before I answer from memory, I'll audit the repo for gaps: key parity between the two files, strings hardcoded in Compose code (lint can't catch those), per-app language support, and which parts follow the language.

Agent

The audit gives a clear answer, and it also turned up two real gaps. I'm confirming the more serious one against the word lists before I claim it.

Agent

Now the tests, including a guard that fails if someone adds a translation folder without listing it in the per-app language config (or the reverse).

Agent

adb isn't on this shell's PATH any more (it was earlier), so I'll add the SDK's platform-tools for these commands and retry.

Agent

The system App languages screen opened for Nature Quest, so the phone was unlocked there. I'll read its text (not a screenshot) to see whether it now lists the app's languages.

Agent

Final checks, then commit and PR through the normal flow.

Agent

Yes, the app already had English and Spanish, but an audit found two gaps in it and I've fixed both on develop (PR #12). The fixes are not in the published v1.0.0 APK or on master yet.

What was already there
- The default English strings and the Spanish strings have identical keys: 89 translatable strings, none missing or extra, and a missing translation fails the build.
- No user-visible text is hardcoded in the UI code.
- The language also covers what the AI writes, the voice that reads it aloud, and the summary. I had seen both languages running on your Pixel before.

The two gaps I found
1. A safety hole on phones in other languages. On a phone set to, say, French, the screens fell back to English, but the model was asked to write in French. My safety word lists only cover English and Spanish, so French hunt items would not have been screened. Now any language other than English or Spanish gets the English app everywhere: screens, hunt, feedback and voice.
2. No per-app language. The app wasn't in Android's "App languages" settings, so someone on an English phone couldn't pick Spanish for just this app. That may be what the participants ran into. It is now listed there with English and Español, on Android 13 and newer; on Android 12 it follows the phone's language.

Checked on your Pixel
- The system App languages screen now lists Nature Quest with English and Español.
- With the app set to French, it showed English screens, an English hunt and English speech.
- The phone's own Spanish still works end to end.
- 154 unit tests pass, 6 of them new. One fails if a translation folder isn't listed in the per-app language config, or the reverse.
- I also added Spanish screenshots and a Languages section to the README.

Not checked: I haven't heard the voices, only confirmed that speech starts, and I haven't tried Android 12. I did not build an in-app language switcher. The system setting is the standard way, but a visible toggle on the setup screen would be easy to add if you want judges to find Spanish quickly.

Decision for you: should I cut 1.0.1? Since the safety gap is in the published release, 1.0.1 is how it reaches users. It also updates the README that GitHub shows by default (master) with the Languages section. It would be a release PR into master, a v1.0.1 tag, a re-signed APK, and a new GitHub Release, and I haven't started any of that.

You

yes, make a cut with v1.0.1

Agent

Cutting v1.0.1 with the same sequence as 1.0.0: release/1.0.1 from develop, version bump, final checks, PR into master, tag, merge back, GitHub Release. First the branch and version bump. The versionCode must increase to 2, or Android won't treat 1.0.1 as an update to 1.0.0.

Agent

The README still says "148 unit tests" (it's 154 now), so I'll fix that in the release too. Then the full verification: clean build, lint, tests, and the signed release APK.

Agent

Signed, valid, version 1.0.1 / code 2, not debuggable, and the language config is packaged. To be able to tell users it updates 1.0.0 in place, I'll confirm both APKs are signed by the same key.

Agent

The release PR (#13) is based on master and is mergeable. Merging with a merge commit, then tagging v1.0.1.

Agent

Tagged. Now merging master back into develop through a PR (never deleting master), then removing the finished release branch.

Agent

develop now contains everything on master, and the release branch is cleaned up. Publishing the GitHub Release, first re-verifying that the APK I'm attaching is exactly the one I verified.

Agent

Published. Verifying what GitHub actually serves: the release metadata, that "latest" now points at 1.0.1, and a fresh download compared against my checksum.

Agent

v1.0.1 is out: https://github.com/RZEROSTERN/hacktoberfest-week2/releases/tag/v1.0.1. It is marked Latest, with the signed APK (55 MB) attached.

I downloaded the APK from the public releases/latest link and it matches the one I built (SHA-256 e64f5739…a893), and it reads as version 1.0.1, code 2. The README on master, the page participants see by default, now has the Languages section with the Spanish screenshots.

Steps:
- Release PR: it came from the release/1.0.1 branch (a version bump plus a README test-count fix). The language work was already on develop through the earlier PR.
- Tag: v1.0.1 points at the master merge commit.
- Back-merge: master is merged back into develop, which has nothing missing from master.
- Cleanup: the release branch is deleted.
- Checks: 154 unit tests pass and lint is clean.

It uses the same signing certificate as 1.0.0 and a higher version code, so it should update 1.0.0 in place and keep the downloaded model. That comes from how Android handles updates; I didn't try it on a device.

Still true, and stated in the release notes:
- Photo-check accuracy is unmeasured because samples/ has no photos.
- I tested the debug build on your Pixel, not the signed release APK. Your phone still has the debug build, and installing the release over it needs an uninstall first, which deletes the model.
- I haven't heard the voices, only confirmed that speech starts.

The keystore backup warning from 1.0.0 still applies. It exists only on this Mac, and losing it means future versions can't update installed apps in place.

You

now, help me to fill this template, retrieving the result in MD format:

<pasted_content id="5de6">

What I Built

<!-- What does it do, and how does it get people off the screen and into the world? Who is it for? -->

Demo

<!-- Share a deployed link or a video demo. -->

Code

<!-- Show us the code! You can embed a GitHub repo directly into your post. -->

How I Built It

<!-- Which open-source AI did you use (open-weight models, agent harnesses, frameworks, local inference), and how is your project built around it? -->

Why Does Open Innovation Matter?

<!-- Why does open innovation matter for what you built? What did it make possible that a closed API wouldn't? -->

My Agent Session

<!-- Optional, but judges love it. Save your session with DevRelay and embed it with the agent_session tag (see the challenge page), or link to it. -->

Prize Categories

<!-- Which partner categories are you entering? List every one that applies, or remove this section. -->

<!-- Team Submissions: Please pick one member to publish the submission and credit teammates by listing their DEV usernames directly in the body of the post. -->
</pasted_content id="5de6">

Agent

To fill this in accurately (especially the prize categories and the agent-session tag, which I shouldn't guess), I'll first check the actual challenge details and DEV's posting rules through your DevRelay tools.

Agent

The skills point to the live challenge data and two knowledge guides (dev-challenges and dev-guidelines). Loading those tools.

Agent

I have what I need from DEV's own sources. The post below is drafted from facts verified in this project, with a few clearly marked gaps only you can fill. After it, I list what to check.

---
title: "Nature Quest: a scavenger hunt where Gemma 4 runs on the phone and the screen stays in your pocket"
published: false
description: "An offline Android scavenger hunt for families. Gemma 4 E2B runs on the phone through LiteRT-LM: it writes the hunt, checks the photos and reads everything aloud. No server, no signal needed, and kids' photos never leave the device."
tags: hf26challenge, android, ai, opensource
ai_disclosure_level: some_ai
---

## What I Built

**Nature Quest** is an offline scavenger hunt for families and kids. You pick a place (park, forest, garden, urban walk), how many things to find (5, 8 or 12) and the age of the youngest player. Then an open-source AI model **running on the phone** writes a hunt list for that place, season and age: "a red leaf", "something with a spiral shape", "bark with moss on it". The app reads the list aloud and asks everyone to put the phone away.

When someone finds something, they tap it on the list and take **one photo**. The same model checks it, answers with a short, encouraging message (plus a friendly hint if it is not a match), and reads that aloud too. When the hunt ends, there is a bronze, silver or gold medal and the time spent outside.

The theme was "Touch Grass", so every design decision started from one question: how do I make the screen the shortest part of the experience?

- Setup is **one screen with everything pre-selected**, so starting a hunt is one tap.
- The list and every piece of feedback are **read aloud**, so nobody needs to look at the phone.
- Big tap targets and a high-contrast palette, because the phone is used outdoors in the sun, for a few seconds at a time.
- One photo per find. No camera roll, no feed, nothing to scroll.

It is for **families and kids on a walk, with an adult**, in **English and Spanish**. The first screen always says: hunt with an adult, only look and take photos, never pick or touch anything.

| Setup | The hunt, written on the phone | Medal and time outside |
|---|---|---|
| ![Nature Quest setup screen with place, number of items and age pre-selected](https://raw.githubusercontent.com/RZEROSTERN/hacktoberfest-week2/v1.0.1/docs/screenshots/01-setup.png) | ![A hunt list with eight items such as a patch of yellow leaves and a textured gray stone](https://raw.githubusercontent.com/RZEROSTERN/hacktoberfest-week2/v1.0.1/docs/screenshots/03-hunt-list.png) | ![Summary screen with a bronze medal and the items found](https://raw.githubusercontent.com/RZEROSTERN/hacktoberfest-week2/v1.0.1/docs/screenshots/04-summary.png) |

## Demo

There is no web demo, because the whole point is that it runs on a phone.

- **Install it:** [Nature Quest 1.0.1 on GitHub Releases](https://github.com/RZEROSTERN/hacktoberfest-week2/releases/latest) (signed APK, about 55 MB, Android 12+, 64-bit ARM). On first launch the app offers to download the AI model (2.59 GB, Wi-Fi recommended). That is the only time it uses the internet.
- **Video:** <!-- TODO: add your video, for example {% embed https://www.youtube.com/watch?v=YOUR_VIDEO_ID %} -->

### Taking it outside

<!-- TODO: fill this in after Saturday's outing with real players. This is a bonus criterion, so be specific and honest. Suggested points:
- Where you went, who played and their ages, how long it took.
- How many items were found, and how the photo checks went (right, wrong, funny).
- What you had to look at the phone for, and what the kids did without it.
- What broke or surprised you. Real quotes from the kids if you have them.
-->

## Code

{% embed https://github.com/RZEROSTERN/hacktoberfest-week2 %}

MIT licensed (plus a Beerware clause). The repo has 154 unit tests, the architecture in the README, and `docs/DECISIONS.md`, a log of 32 technical decisions with the reasons behind each, written as I went. `docs/BENCHMARKS.md` has every number quoted below.

## How I Built It

**The open pieces.** The model is **Gemma 4 E2B** (open weights, Apache-2.0) in LiteRT-LM's `.litertlm` format. It runs through **LiteRT-LM** (Apache-2.0), Google's on-device runtime, with no network and no server. The rest is a normal Android stack: Kotlin, Jetpack Compose, Hilt, CameraX and Android's own text-to-speech. Nothing closed runs in the app. (I built it with Claude Code, which is not open source. It was my pair programmer, not part of the product.)

**The shape of it.** The app talks to the model through one interface, `InferenceEngine`, so ViewModels and use cases never touch LiteRT-LM and tests use a fake. The model answers in strict JSON, which I parse with kotlinx.serialization. If the JSON is bad I retry once, then fall back to something safe: "I'm not sure, try another photo", or a built-in hunt list. Prompts are versioned text files, not strings in Kotlin.

**I benchmarked before I built.** The first real step was a debug-only screen that loaded the model and timed it on my Pixel 10. That changed the design:

- The **CPU backend was about twice as fast as the GPU**: a hunt list took 5.7 s on average on CPU and 11.9 s on GPU, and the first GPU load took 25 s against 3 s. Google's published numbers for a flagship phone did not carry over to mine.
- **Decoding is slow** (7 to 23 tokens per second), so every answer is kept short. "Twelve words at most" in a prompt is a performance feature.
- The phone **slows about 1.6x** after 25 minutes of back-to-back runs, even though the OS reported no thermal throttling. So I picked settings that hit my targets in the slow state: 140 visual tokens and a 640 px photo. A photo check takes 4.5 to 5 s on a cool phone and 5.7 to 6.5 s on a warm one.
- Peak memory is 2.2 to 2.6 GB, so the app loads the model for each use and releases it right after. A warm reload is about half a second, and memory drops to about 240 MB.

**Safety cannot live in the prompt.** The prompt asks for safe items. The model still produced "a coiled snake", "orange fungus" and "a bird's nest hidden in a hollow", and later "a cloud shaped like a deer". For a kids' app that is not acceptable, so every item now passes a deterministic validator in code (English and Spanish word lists: no picking or touching, no animals, insects or mushrooms, nothing near water or roads, no climbing, nothing to eat, no photos of people). Rejected items are regenerated, and if the model still falls short, a built-in safe list fills the gap. The model's *feedback* is checked too, because it is read aloud to children.

**Bugs the phone found that tests did not.**
- The same settings produced the **identical hunt three times in a row**. LiteRT-LM's sampler seed defaults to 0, which makes sampling deterministic. Each request now gets a random seed.
- A regex that passed every JVM test **crashed on Android**, because Android's regex engine rejects a bare `}` that Java accepts.
- Cancelling the Kotlin Flow does **not** stop native inference. You have to call `cancelProcess()` yourself.
- CameraX's in-memory JPEG already carries its EXIF rotation, so rotating it again turns the photo sideways.
- A Compose cleanup effect unbound the camera I had just bound. It read state when disposed instead of when created.

**Privacy and delivery.** Photos are processed in memory. A small copy of each found photo stays in private cache only for the summary and is deleted when you close it, unless you tap "Save to gallery". The only code that opens a network connection is the model downloader: it resumes interrupted downloads, pins an exact model revision, and verifies the SHA-256 before using the file. I tested that on the phone by killing the app at 17% and resuming. The manifest asks for `CAMERA`, `INTERNET` (download only) and network state.

**Limits, stated plainly.** I could not measure how *accurate* the photo check is: I did not have a set of real photos to score the model against, so treat its verdicts as fun and encouraging, not as a judge. I tested on one phone. Reading aloud needs a voice installed for the phone's language.

## Why Does Open Innovation Matter?

Four things an open model made possible that a closed API would not:

- **It works with no signal on the trail.** After a one-time download, there is no server to reach. The people this is for are standing in a field.
- **Kids' photos never leave the phone.** With a hosted model, "check this photo" means sending a child's picture to someone else's server. Here it is a function call on the device, and the only network code in the app is the downloader. Because the code is open, anyone can check that claim.
- **It costs nothing per hunt.** No API bill, so a family, a school or a scout group can use it as much as they like. The cost is the phone's time: about 5 to 12 seconds per hunt list and 5 to 7 per photo.
- **It is swappable and pinned.** The model is a file plus one entry in `ModelSpec` (URL, size, SHA-256), behind an interface. The app behaves identically next year, because the exact revision is pinned and checksummed instead of changing under me. And I could turn the knobs a hosted API hides: backend (the CPU-versus-GPU finding above), image resolution and memory.

The honest counterpoint: a server GPU would be faster, and probably more accurate. I traded speed and a 2.6 GB first download for privacy and offline use. For a hunt in a forest, I think that is the right trade, but it is a trade.

## My Agent Session

I built Nature Quest with Claude Code, from an empty repository to two signed releases. <!-- TODO: save the session with DevRelay and embed it here, for example {% agent_session YOUR_SESSION_ID %} (or link to it). Check the transcript for secrets and private paths first, and make the session public or judges cannot open it. -->

## Prize Categories

- **Best Use of Gemma.** Gemma 4 E2B is the core of the app, not a bolt-on. It writes every hunt, answers every photo check, and runs entirely on the phone through LiteRT-LM. The write-up above covers how I tuned it: CPU versus GPU, visual-token budget, short answers, and per-use loading.

---

*Disclosure: I built this with Claude Code as a pair programmer, and Claude helped draft this post. I reviewed and edited it, and the numbers come from the benchmarks in the repo.*

Before you publish
- The two gaps only you can fill: the video link, and "Taking it outside". The rubric gives bonus points for actually using it outdoors, and the Saturday outing is the natural source. I did not invent any field results.
- Check the disclosure line is true. The last line says you reviewed and edited it, so please read it and change whatever doesn't sound like you. The front matter already sets ai_disclosure_level: some_ai, which DEV requires for an AI-drafted post.
- Tag and deadline: hf26challenge is the required tag, and DEV allows only four tags. Submissions close Oct 11, 11:59 PM PDT, which is Oct 12, 00:59 in Mexico City. It's a new-project challenge, and the repo was created on Oct 5, after the window opened, so you're fine. Commits made after the deadline would need a note in the README.
- I haven't published anything, saved your session, or staged a draft. I can stage this as a draft through DevRelay for you to review. For the agent session, the transcript is long and includes local file paths and your email address from the session context, so I'd want to scrub those before uploading anything. Say the word on either.

Prize Categories

  • Best Use of Gemma. Gemma 4 E2B is the core of the app, not a bolt-on. It writes every hunt, answers every photo check, and runs entirely on the phone through LiteRT-LM. The write-up above covers how I tuned it: CPU versus GPU, visual-token budget, short answers, and per-use loading.

Disclosure: I built this with Claude Code as a pair programmer, and Claude helped draft this post. I reviewed and edited it, and the numbers come from the benchmarks in the repo.

Top comments (0)