Inspiration

Duolingo is not fun because it teaches Spanish well. It's fun because it turns learning into a game you feel bad about skipping: streaks, points, leveling up, a little dopamine hit every time you finish a lesson.

We wanted that same feeling attached to something that actually helps people instead of just helping your Spanish. Philanthropy usually doesn't have a "will" problem, it has a fun problem. Most volunteering apps read like a job board. We wanted GoodDeed Go to feel like a game you're excited to open: a live map of nearby quests, points for doing real good, a streak to protect, and a leaderboard to climb. Duolingo for good deeds.

What it does

Quest map. Real nonprofits near you, pulled live from Google Places. Ranked so small, grassroots orgs like food pantries and mutual aid groups show up right alongside big institutional ones, instead of getting buried under review counts. Green pins are vetted nonprofits, coral pins are problems neighbors posted themselves.

Geofenced, server-timed quests. The Start button on a quest is locked until you're physically within 200 meters of the location. Once you start, the timer runs on the server, not the phone, and it stops counting if you walk away. Longer, timed shifts earn more points than a quick drop-in, and none of it can be faked from your couch.

Everyday quick actions. Not everything needs a trip. A lightweight "Today" tab covers small stuff, holding a door, picking up litter, no photo required, honor system, capped at 20 points a day so it's not worth farming. A companion creature grows with your total points and goes sleepy if you break your streak.

Photo-verified submissions. For bigger deeds, there are 8 distinct deed types (donation, cleanup, fundraiser, and so on), each judged against its own rubric, a donation receipt isn't scored the same way as a cleanup photo. Do the deed, snap a photo, add a description, and a vision AI checks that the photo actually matches what you claim, and that the time you logged is believable, before you get any points. If it doesn't match, it scores zero and tells you why in plain language, but nothing already earned is ever taken away.

Points, tiers, leaderboards, streaks. Bronze, Silver, Gold progression. Friends and nearby leaderboards, daily and weekly. Points power the leaderboard; a separate tier-points total governs Bronze/Silver/Gold so a bonus multiplier can't fast-track anyone.

Community help feed. Anyone can post a local problem, like trash under a bridge, and an AI check filters out spam before it goes public. A neighbor claims it, has three hours to submit proof, and the original poster confirms it's done. The helper gets the AI's score (never less than a floor amount), and the poster gets a smaller credit just for spotting the problem in the first place, both show up on the leaderboard.

Boost and progression. Users can put points behind their own fundraiser to get it shared further; the link gets a web-search legitimacy check before it goes live, and the proof screenshot gets checked after. Points convert into coins for cosmetic avatars and eggs, and spending coins never lowers your tier. Friends can join a shared leaderboard with a team code.

How we built it

Two of us, three layers, one shared JSON contract enforced by a test so nobody could silently break the other's work.

AI agent (pure Python). find_opportunities fans out 50+ Google Places queries across 15 philanthropic categories to surface real nonprofits, tuned so a 9-review community garden isn't drowned out by a 1,400-review institution. Legitimacy scoring runs two layers deep: a fast arithmetic score off the Places record itself (review count on a saturating curve, rating, whether a website is listed, a hard zero for permanent closure), and an optional trust_check that sends the org to an LLM with real web search to look for independent evidence, 501(c)(3) status, Charity Navigator, GuideStar, news coverage, before it's trusted enough to become a quest. score_submission sends the photo to a vision LLM to judge authenticity, not impact, since a photo can't prove real-world impact, it can only prove the deed plausibly happened. classify_report triages community reports and fails closed, so nothing unmoderated ever reaches the public feed. Every external call sits behind a provider interface, so the whole thing runs against deterministic mocks with zero API keys, and swaps between OpenAI and Claude with one environment variable.

Backend. FastAPI and SQLAlchemy handling auth, leaderboards, tiers, streaks, badges, and the claim lifecycle for community reports. Claims auto-expire after 3 hours so nobody can permanently lock a request. Server-side geofencing and quest timers so location and duration can't be spoofed client-side.

Frontend. Plain HTML, CSS, and ES modules, no build step, Leaflet plus OpenStreetMap tiles so no map API key is needed. Built mobile-first against our Figma.

One FastAPI server serves the API and the static frontend from the same origin, deployed as a single Render service backed by Supabase Postgres, with Google Sign-In for auth.

318 tests across the three layers, all running with no network calls and no API keys, because everything external is mocked behind a provider seam.

Challenges we ran into

Phantom caching bugs. We had the frontend running on a separate static server on a different port than the API at one point, and browsers kept serving stale builds. Cost us hours before we traced it and collapsed everything down to one server, one origin, no build step to go stale.

Legitimacy versus visibility. Sorting by raw review count buried every small nonprofit under the biggest institutions nearby, which defeats the point of a hyperlocal app. We had to treat review count as weak, saturating evidence instead of a ranking signal, and interleave results across categories so the map shows a mix instead of 20 food banks in a row.

Making AI failure non-fatal mid-demo. A model outage can't take down the whole app in front of judges. Every agent function degrades to an explainable zero-value result instead of throwing, except report classification, which fails closed so nothing unmoderated ever reaches the public feed.

Splitting points from tier points early, so a bonus-event multiplier boosts your leaderboard rank without letting you fast-track to Gold on one lucky weekend.

Building eight different rubrics that don't feel arbitrary. A donation receipt, a cleanup photo, and a fundraiser screenshot all need to be judged on different evidence, so each deed type got its own prompt instead of forcing one generic rubric to cover everything.

Two people, three layers. With only two of us covering the agent, backend, and frontend, the shared data contract, field names locked down and enforced by a dedicated test, was the only handshake we needed between layers. That let us build and test the agent, backend, and frontend in parallel from hour one instead of serializing the work.

Render's free-tier cold start (about 50 seconds) nearly ate a live demo. We fixed it by warming the server up right before presenting.

What we learned

A gamification loop is only as trustworthy as its verification step. The whole "Duolingo for good deeds" idea falls apart the moment points can be faked, so the vision-authenticity check, and the server-side geofencing behind it, ended up being the real hard problem, not the map or the leaderboard.

Putting a mock-versus-real switch behind every external call on day one let both of us build against a fully working app immediately, instead of one of us waiting on the other to wire up API keys.

Small nonprofits need to be designed for, not just included. Ranking by raw popularity quietly erases the exact organizations a hyperlocal tool is supposed to surface.

What's next

Add rate limiting. Right now every submission spends an LLM call with nothing throttling it.

Move Google OAuth out of testing mode so it isn't gated to a test-user allowlist.

Reconcile the Figma tier numbers with the shipped contract. They currently disagree, and the backend is the source of truth.

Built With

Share this project:

Updates

Submission history