Skip to main content
Grass Pass

How it works

How a park becomes a pass.

Real park data in. One open AI model writes. Code checks every line. Paper out.

  • Gemma 4 · open weights, Apache-2.0
  • 10 outside services, all listed below
  • 26 code checks on every clue

The blueprint

Every service, and where the AI sits

Follow the arrows: real data, code, one open model, code again, paper.

  1. Stage 1: Real data

    Real park data

    • OpenStreetMapNominatim search + Overpass map
    • iNaturalistSightings, 1.5 km, 14 days
    • WikipediaSpecies summaries, via iNaturalist
    • SerpApiGoogle Maps review counts
    • Open-MeteoForecast
    • weather.govNational Weather Service alerts
  2. Stage 2: Code

    Code builds the fact list

    • Each fact gets an id, source and date.
    • Removes 71 risky species groups first.
    • Picks the spot, draws the map.
  3. Stage 3: Open model · only AI step

    Gemma 4 31B

    gemma-4-31B-it

    Apache-2.0 open weights · DigitalOcean serverless inference

    • Strict JSON: only the park's own ids
    • 1-3 clue calls + 1 tips call a pass
  4. Stage 4: Code

    Code checks every line

    • 26 hard rules (quotes, names, numbers, safety). Fail one: removed.
    • Too few left? Asks again.
    • Writes dates, safety lines and answers.
  5. Stage 5: Paper

    Your printed pass

    The kid's hunt on top; answers and sources below.

Runs on

  • DigitalOceanServerless inference (the model)
  • VercelHosting + daily cron
  • Upstash RedisCache, saved passes, limits
  • Auth.jsGitHub or Google sign-in, "Try as a judge"

The AI picks and writes. Code does the rest.

The AI cannot invent finds because it can only use ids from the park's own fact list.

  1. Job 1: Picks finds, writes clues

    In: Wikipedia, via iNaturalist

    Fact, with the model's proof quote marked: “The distinctive fruit, from a multiple fruit family, is roughly spherical, bumpy, 8 to 15 centimetres (3–6 in) in diameter, and turns a bright yellow-green in the fall.”

    Out: gemma-4-31B-it

    “Hunt for a tree with bumpy, yellow-green fruit.”

    Same fact, no AI (our template)

    “____, commonly known as the ____ orange, is a small deciduous tree or large shrub, typically growing to 8 to 15 metres”

    Code

    • Quote found, word for word.
    • Answer key: Osage-orange.

    Recorded at Connemara Meadow Preserve, Oct 6, 2026, ages 6-10 · Eval results

  2. Job 2: Writes the riddle

    In: OpenStreetMap, described by code

    Fact about the picnic shelter, with the model's proof quote marked: “It has a roof held up by pillars and tables below it where people share a meal.”

    Out: gemma-4-31B-it

    “I have a roof and pillars to keep you dry and cool while you eat a meal.”

    Code

    • Picked the picnic shelter, drew the map.
    • Quote found, word for word.

    Recorded at Celebration Park, Oct 6, 2026, ages 6-10 · Raw answer

  3. Job 3: Writes trip tips

    In: Open-Meteo

    Fact w-sunset: “Sunset at 7:02 PM”

    Out: gemma-4-31B-it

    “Finish your hunt before sunset at 7:02 PM”

    Based on fact w-sunset

    Code

    • Wrote the facts first.
    • Kept 6 of 6 tips: each names a real fact.

    Recorded at White Rock Lake Park, Oct 8, 2026, ages 13+ · Raw answer

Runs on DigitalOcean

Every Gemma call on this site runs on DigitalOcean

Serverless inference: no GPU of our own. The website itself is hosted on Vercel.

OpenAI-compatible endpointhttps://inference.do-ai.run/v1

  • One host gets the key

    The DigitalOcean key is only ever sent to inference.do-ai.run.

  • Spend has a ceiling

    Per token, on prepaid credit; at most 400 model calls a day.

  • Measured

    9.5 s a typical call. $0.00107 a pass for the clue calls: missed the $0.00100 target. Trip tips add one short call.

  • Swappable

    MODEL_BASE_URL and MODEL_ID pick another host or model, under the same checks (Llama 4 Maverick ran them in our eval).

Why an open model

Open weights, our own rules

  • The licence is open.

    Gemma 4's weights are Apache-2.0: anyone can download, run and build on them.

  • It can be self-hosted.

    Any OpenAI-compatible server, like Ollama. On a laptop CPU it costs $0, but it is slow.

  • Our rules, not a vendor's.

    The same checks run on any model; Llama 4 Maverick went through them in our test.

Measured

Real numbers, misses included

Eval run 2026-10-10-3: 20 real parks, ages 6-10, 60 runs (full results).

  • 98%

    of clues quote their source exactly

    MetTarget 85% or more

  • 0

    risky species printed in 60 test runs

    MetTarget 0, always

  • $0.00107

    per pass for the clues, at list price

    MissedTarget $0.00100 or less

  • 2,754

    unit tests passing, in 98 files

    Counted Oct 10, 2026

What it runs on, and what it costs

Every outside service Grass Pass uses, what it is for, and its licence or cost
ServiceWhat it's for
OpenStreetMapDataPark search, the park map, Find This SpotODbL 1.0, free
iNaturalistDataWildlife really seen nearbyFree API
WikipediaDataSpecies facts clues must quoteCC BY-SA
SerpApiDataLucky Finds (counts, no review text)Free plan (250 a month)
Open-MeteoDataWeather card, trip-tip factsCC BY 4.0, free
weather.govDataOfficial US weather alertsUS public domain
Gemma 4 31BOpen modelClues, riddle, trip tipsApache-2.0 open weights
DigitalOceanPlatformRuns every Gemma call the site makes: no GPU of our ownPer token, prepaid credit; at most 400 model calls a day
VercelPlatformHosting, daily example warm-upHobby plan
Upstash RedisPlatformSaved passes, caches, limitsFree plan (500,000 commands a month)
Auth.jsPlatformGrown-up sign-in: GitHub or Google sign-in, "Try as a judge"ISC licence

Source code on GitHub (MIT)Make a pass

Fine print

Limits, privacy and every detail

Where we fall short

Cost and slow calls missed the goal. All of them: About page.

All 12 limits, with the numbers

  • Cost missed the goal. $0.00107 a pass for the clue calls (target $0.00100; up to $0.00111 if 4 timed-out calls were billed in full). The trip tips add one more short call.
  • Some clues are still vague. 13 of 123 printed Wild Finds, like "Check for a small bird that is yellow."
  • Some clues repeat across parks. 1.6% of printed clues (target 5%: met; 4.1% the run before).
  • Model speed depends on DigitalOcean. 9.5 s typical, 25.3 s slow (target 10 s / 20 s: typical met, slow missed; 9.9 s typical the run before). A first call gets up to 40 s: in 60 test runs (53 passes; 6 runs on the 2 no-data parks made none), 3 first calls hit the limit and 1 run was lost (0 the run before).
  • Some passes come out short. 90.2% of data-rich test runs made a complete pass (target 90%: met); a short one says how many finds are missing.
  • The kid check is not done yet. Planned for Sunday, Oct 11, on Kevin's park walk; still pending.
  • Lucky Finds run on a free plan. 25 searches a day; a count is a "maybe", not a promise.
  • Public map servers can be busy. Mostly US evenings: search falls back to a saved Dallas-area list.
  • Find This Spot is not in the eval. No map data was recorded for the test parks.
  • Self-hosting on a laptop CPU is slow. We ran the small Gemma 4 E2B (gemma4:e2b-it-qat, Apache-2.0) with Ollama on a Windows laptop CPU (Intel Core Ultra 7 155H, 32 GB RAM, no GPU), on 5 test parks, for $0. With a 70 s limit per model call (the most the app allows; the hosted site gives a first call 30-40 s), 0 of 5 passes were complete: 3 ran out of time and the other 2 came out short. Given more time (an eval-only setting), 4 of 5 were complete, 96.7% of clues quoted their source, reading grade 2.9, 0 risky species printed, at 59.3 s a typical call (hosted Gemma 4 31B: 9.5 s). The app now has a longer clock for a model on your own computer (LOCAL_MODEL_TIMEOUT_MS, off by default): in one browser try, a Celebration Park pass came out with 7 of 8 finds and its map in 96 s (2 model calls). The model used about 5-6 GB of RAM.
  • After 1 free pass a day, a grown-up signs in. 5 new passes a day each; judges can press "Try as a judge".
  • The model runs on DigitalOcean's servers. The park facts and age band leave your device.

Nothing about your child

The AI sees park facts and the age band, nothing about you or your child. Everything that leaves your device: the About page.

Privacy in short

  • No names, photos, or analytics. Browsing and printing set no cookie.
  • Using your free pass sets one small signed cookie: the date and a count, no ID. It expires at midnight Dallas time.
  • Signing in (grown-ups: more new passes, reports and ratings): We only keep a scrambled ID to count your new passes, reports and ratings. No email, no name. No password is stored.
  • What you type goes to our server and OpenStreetMap, never into the web address.
  • "Use my location" is rounded to about 1 km in your browser before it is sent.
  • The AI sees park facts and the age band, nothing about you or your child.

Under the hood

From “which park?” to “found it!”

Each step is handled by code, the open model, or you. The AI has one job: picking the finds and writing the words.

  1. Step 1. Pick your parkDone by code

    Search by town, ZIP, park name, or tap Use my location (rounded to about 1 km). Pick 1, 3, 5 or 10 miles, and code finds the parks in that circle using OpenStreetMap.

    Nominatim finds the place; the Overpass API lists the 20 nearest named parks and nature reserves within the distance you picked, as numbered cards and pins on a street map (MapLibre with OpenFreeMap tiles). If Overpass takes over 10 s, code falls back to a saved list of 1,321 Dallas-area parks, then a Nominatim park search, and says so.

  2. Step 2. Gather what's really thereDone by code

    Code builds three dated, sourced lists. Park Finds come from OpenStreetMap, Wild Finds from iNaturalist and Wikipedia, and Lucky Finds from Google review counts via SerpApi.

    • Park Finds: what is mapped inside the park, with the map's count and a short fact sheet per kind.
    • Wild Finds: research-grade iNaturalist species within 1.5 km, last 14 days, each with the start of its Wikipedia summary. If the summary never says how it looks, it is left out.
    • Lucky Finds: "maybe" finds (a dog, a bike, ducks, a skateboard) that at least 3 Google Maps reviews from the last 2 years mention, counted via SerpApi. Review text is never stored, shown or sent to the AI.

    From September 15 to November 15, code also counts monarchs seen within 25 km in the last 14 full days (next to the same days last year) and checks for milkweed within 1.5 km. The AI never sees that box.

  3. Step 3. Take out anything that bites, stings or itchesDone by code

    Code removes 71 groups of risky species, like fire ants and poison ivy, before the model sees the list and checks again after.

    Code checks each species' iNaturalist taxon id and all its parent groups against the blocked list (venomous snakes and spiders, fire ants, poison ivy, stinging plants and caterpillars...). The check runs again on every find the model picks, and a clue that even names a blocked thing is removed.

    Code writes the safety lines ("Look, don't touch.", "Stay with your grown-up near water."). The model never decides what is safe.

  4. Step 4. The AI writes the cluesDone by the open model

    Code sends the park's fact list to gemma-4-31B-it (Google's Gemma 4, open weights, Apache-2.0) on DigitalOcean serverless inference. It picks the finds by id and writes a clue and a proof quote for each, plus one riddle. Usually that is one call; step 6 is the exception. The trip tips are one more call.

    There is no automatic switch to another model: if the call fails, the pass says so. MODEL_ID changes the model, and every pass names the one that answered.

    What the model is given: the park's name; each fact's id, section, kind and text; the age rules (6 finds for 4-6, 8 for 6-10, 8 with 2 hard ones for 10-13, 8 with 3 hard ones for teens and adults (13+), and a reading level); the month; and the Find This Spot place, if code picked one. For the bands with hard finds, the model is asked for one more hard find than the pass promises (3 for 10-13, 4 for 13+), as a spare, so one dropped hard clue still leaves the promised number.

    What it is NOT given: review text, reviewer names or review counts, and nothing about you or your child (no name, location, IP address or anything you typed).

    What it sends back: strict JSON whose schema only allows the real ids. For each find: the id, a clue, an optional "look where" hint, easy / medium / hard, and a proof quote from its facts. Plus the riddle. Its time limit fits the answer it asks for: up to 40 s for the first call.

  5. Step 5. Code fact-checks every clueDone by code

    Code never takes the AI's word for it. It removes any clue that breaks one of 26 hard rules (proof quote, name leaks, numbers, safety), and swaps out clues that miss 7 style rules when it can.

    A clue is removed when:

    • The answer isn't in the shape we asked for (a part missing, a clue too long). schema
    • The clue stops mid-sentence ("Hunt for a"). cut_off
    • It has a link, an email address or web code. url_or_markup
    • It points at something not on this park's list. unknown_id
    • It uses the same find twice. duplicate_id
    • It puts a find in the wrong section. section_mismatch
    • It is about, or names, a blocked species ("poison ivy"), or says poisonous, toxic, venomous or stings. danger
    • It tells the reader to touch, pick, crush, catch, chase, pet, hold, eat or collect something ("Crush a leaf and smell it"). A hint that says so is left out; a riddle that says so is replaced by our fixed line. handling
    • Its proof quote isn't in that find's facts, word for word. not_grounded
    • A Park Find clue describes a different thing (a roof and pillars for a bench): its facts never say that. other_feature
    • It gives the answer away: a word of the name, the phrase the name is made of ("a bird with a red tail" for Red-tailed Hawk), or talk about the name. name_leak
    • It says "map" on a pass with no map. mentions_map
    • It mentions flowers or fruit that local photos don't show this month. out_of_season
    • It has a number that isn't in its facts. number_not_in_source
    • Its count isn't the map's, or counts the wrong thing. wrong_count
    • A "how many" that gives its answer away or has nothing to count, or a question mixed with a count. broken_count
    • It asks the child to listen for something silent, or whose facts name no sound. silent_sound
    • Nothing real was left once the filler opener ("Quick!") was cut. filler_only
    • It opens with words people don't say ("Explore for a bug"). odd_wording
    • A second clue where the thing talks as itself ("I am ..."). riddle_frame
    • A Wild Find clue with no detail from its facts ("a tree with leaves"), or a quote that is just the name. generic_clue
    • It reads like a field guide, not a walk: a family or Latin group name ("a moth of the Crambidae family"), a weight or a pile of measurements, or a word like "pterostigma". jargon
    • It calls the find the wrong kind of thing ("a bug" for a spider, "a pet" for a wild fish, "a fungus" for a lichen). wrong_kind
    • It copies an example sentence from our instructions. copies_example
    • It starts with the same first word as another clue (a stock start like "Can you find" only goes first when a spare exists). repeats_opening
    • Its section already has as many finds as the mix allows. over_section_max

    Style rules (not truth): a clue that misses one goes first when a spare can replace it.

    • It uses a colour or size word from the thing's own name ("white flowers" for White Morning-glory), or a word that starts like it ("a globe" for Globular Drop Snail). name_trait
    • On a teens & adults (13+) pass: it asks the reader to listen. That pass is about what you can see. listening
    • Nothing to look at: where it lives on a map ("native to Texas and Oklahoma") or one bare colour ("a bird that is black"). A field-guide word ("operculum") only goes first when a spare exists. (removed, except on a park with little data) trivia
    • It copies 4+ words in a row from its facts instead of using kid words. copies_source
    • It nearly repeats another clue on the pass or its sentence pattern, or is a second "listen for" clue. A third pond, creek or fountain find, or a second "glitters like a mirror" picture, only goes first when a spare exists. (removed, except on a park with little data) repeats_clue
    • It starts with a worn-out frame we tell the model not to use ("Somewhere you will see a"). If no spare replaces it, code swaps the frame for a plain word ("Spot a ..."). stock_frame
    • Wording that doesn't fit the age band: sign words on a kid pass ("flat smooth areas", "strung across its center"), kid-style wording on a teens & adults (13+) pass ("a ride with two wheels", "Who has ...?", "little"), "Check for", a sentence piece, one word twice in a clue, or people who may not be there. kid_wording

    Safety, proof quotes, name leaks, numbers and counts are never relaxed. A "look where" hint that names the answer (or says "map" with no map) is dropped; its clue stays.

    Code never rewrites what a clue says. It makes four small edits: it cuts a filler opener ("Quick!", "Psst,"), turns "?" after a command ("Track 3 fields?") into a full stop, swaps a worn-out opening ("Somewhere you will see a") for a plain word ("Spot a"), and says "What", not "Who", for a plant, fungus or lichen.

  6. Step 6. Refill, or print it shortDone by code

    Too few clues pass the check? Code asks the AI again for the missing ones (at most 3 clue calls per pass). Still short? We list how many are missing. No made-up filler.

    Little data: the model is asked for one spare. More than one find lost: code asks again, at most 3 clue calls per pass (plus 1 for the trip tips):

    • Some clues kept: a refill for the missing finds (plus up to two spares) from unused facts, naming what was copied or too generic.
    • If nothing was kept, it is the whole request again.
    • A first call that fails (timeout, network or server error, or an unusable answer) gets one whole retry, cut to the finds an answer can carry in the time left. A pass still short after a refill gets one more refill. Limits come from measured answer sizes and slow-evening speeds: a refill gets 15-30 s, the whole pass 85 s. A short "403" refusal is asked once more after 1 s.

    Nothing is padded with made-up finds.

  7. Step 7. Find This Spot and the October boxDone by code

    Code picks a landmark, draws a map with an X, and measures the walk. The October box is all code.

    Code picks a landmark the park has only one of (a shelter, a playground, a bridge), or else a sports field. It draws the paths with an X there, a START at the nearest parking lot or entrance, a north arrow and a scale, and measures the walk.

    The model writes the riddle in the same call, under the same checks. If it fails or the map is late (about 2 s before the call, 3 s after), code uses a fixed riddle. No landmark: "No Find This Spot today".

    The October box (step 2) is all code, numbers and dates included.

  8. Step 8. Print and ditch the phoneDone by you

    One black-and-white page: the kid's hunt on top, your answer key and sources below. Then the phone goes in the bag.

    US Letter (A4 works too). Top: finds with tick boxes, safety lines and sources, the map and riddle, the October box. Bottom: answers, safety notes, dated sources, and which model answered when.

    Code writes every number and date on the page.

  9. Step 9. Saved passes and fair limitsDone by code

    Passes are saved for 30 days. Without signing in you get 1 free new pass a day; a grown-up who signs in gets 5 new passes a day. Daily limits protect the model budget and the free map servers.

    A pass is saved per park, age band and day (Dallas time) for 30 days, so the next visitor gets it at once. "Make a different pass" allows 3 a day. Park data is cached for 6 hours (sightings) to 30 days (review counts, place searches); a failed lookup is remembered for about 15 minutes.

    • 400 model calls a day for everyone (10% kept for the examples).
    • Per internet address: 20 new passes a day (3 a minute) and 60 park searches; 2000 searches a day for everyone.
    • Lucky Finds: 25 SerpApi searches a day, 200 a month (free plan).
    • A pass made while a source was down is rebuilt at most 3 times a day, not counted toward anyone's daily passes.
    • Without signing in: 1 free new pass a day per browser, counted by one small signed cookie (the date and a count, no ID), and at most 3 signed-out new passes a day per internet address.
    • Signed in with GitHub or Google: 5 new passes a day each. Judges can press "Try as a judge": All judges together can make 60 new passes a day with it, at most 3 from one internet connection.

    Reports: A find is left off new passes for that park when at least 3 different signed-in visitors didn't find it in the last 30 days and they are more than 60% of those who reported on it, or at once when 2 say it is not safe (we then review it). Each visitor counts once per find. Judge demo reports are only logged, never counted. Reports are deleted after 90 days.

    At a limit, the page says so; saved and example passes keep working.

What we measured: run 2026-10-10-3

On 20 parks, ages 6-10: $0.00107 a pass for the clue calls (trip tips add one more short call), reading grade 2.5 (no-AI template: 2.5). All numbers: the About page.

20 real parks, ages 6-10 (full results):

Gemma 4 31B, 60 runs, run 2026-10-10-3 (2026-10-10)
WhatMeasuredTargetResult
Cost per pass for the clues (DigitalOcean list prices; trip tips add one more short call)$0.00107 (up to $0.00111 if 4 timed-out calls were billed in full)$0.00100 or lessMissed
Model time per call, typical / slow9.5 s / 25.3 s10 s / 20 sMissed
Complete passes (at most 1 find missing)90.2% (46/51)90% or moreMet
Reading level (grade, median)2.53.5 or lowerMet
Clues quoting their source word for word, before the checks98%85% or moreMet
Blocked species printed00Met
Clues repeated across parks1.6%5% or lowerMet

Speed is met for a typical call and missed for the slow ones: the typical call took 9.47 s, first calls alone 11 s, with DigitalOcean answering at 44.8 answer tokens a second (39.5 the run before, when speed was missed). Time limits are sized to each call (up to 40 s for a first call). This is the first full run after the Oct 10 clue-wording rewrite; the run before is 2026-10-06-9. Lucky Finds and Find This Spot are not in this test.

Ages 10-13, a smaller partial check (run 2026-10-06-partial-2320, 2026-10-06, 3 parks, one run each, results): 3 of 3 passes complete in 6 model calls, all 3 with their 2 hard finds, grade 3.6, 12.2 s typical (over the 10 s target) and 18.5 s slow (under 20 s). A finished 10-13 pass cost about $0.00145, which is over the $0.00100 target (a 10-13 pass is longer). Before the checks, 5.4% of its clues named their answer (over the 5% target); code removed them all. Only 6 model calls, so a small sample.

Teens and adults (13+), a smaller partial check (run 2026-10-07-partial-1617, 2026-10-07, 3 parks, one run each, results): 3 of 3 passes complete (at most one find short; 2 printed every find) in 6 model calls, 2 of 3 with their 3 hard finds, 9.5 s typical and 11.8 s slow (within the 10 s / 20 s targets), 2.4% of clues named their answer before the checks (code removed them). A 13+ pass cost about $0.00162, which is over the $0.00100 target. Its clues read at grade 3.9; the grade 3.5 reading target is for kids and does not apply to 13+. Only 6 model calls, so a small sample.

Built in the contest week, with AI coding agents

Built during the Hacktoberfest 2026 Week 1 entry period (first commit Oct 5, 2026). Kevin made the decisions; AI coding agents wrote and reviewed most of the code.

  • AI coding agents (Claude Code) wrote most of the code as "builders" and reviewed it as "auditors".
  • Each audit round runs five reviews (contest rules, security, quality, accessibility and design, and a judge simulator); builders then fix the findings. Twelve rounds so far (Oct 9, 2026).
  • GitHub Actions runs lint, type checks, 2754 unit tests (counted Oct 10, 2026) on recorded real API answers, a production build, and browser tests with accessibility checks on every push to main. Each run's result, green or red, is public on the repo's Actions tab.
  • Claude never writes a pass: every clue comes from the open model named on that pass.